diff --git a/src/arr/compiler/anf-loop-compiler.arr b/src/arr/compiler/anf-loop-compiler.arr index e25e2b275a..b80cd474f6 100644 --- a/src/arr/compiler/anf-loop-compiler.arr +++ b/src/arr/compiler/anf-loop-compiler.arr @@ -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) -> D.MutableStringDict: + 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: + 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: @@ -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: @@ -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): @@ -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 |# diff --git a/src/arr/compiler/anf.arr b/src/arr/compiler/anf.arr index 00b7b46d0a..be7cf8374a 100644 --- a/src/arr/compiler/anf.arr +++ b/src/arr/compiler/anf.arr @@ -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 @@ -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))) @@ -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)) diff --git a/src/arr/compiler/ast-anf.arr b/src/arr/compiler/ast-anf.arr index 182c85d9e4..05de399ccb 100644 --- a/src/arr/compiler/ast-anf.arr +++ b/src/arr/compiler/ast-anf.arr @@ -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 @@ -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 @@ -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) @@ -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) @@ -761,6 +771,7 @@ fun freevars-ann-acc(ann :: A.Ann, seen-so-far :: NameDict) -> 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 @@ -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 @@ -968,7 +979,7 @@ fun freevars-v-acc(v :: AVal, seen-so-far :: NameDict) -> NameDict 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 diff --git a/src/arr/compiler/ast-util.arr b/src/arr/compiler/ast-util.arr index 4210f05269..c4c69f82de 100644 --- a/src/arr/compiler/ast-util.arr +++ b/src/arr/compiler/ast-util.arr @@ -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") @@ -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: diff --git a/src/arr/compiler/compile-structs.arr b/src/arr/compiler/compile-structs.arr index ca8a85f90b..f854370e2b 100644 --- a/src/arr/compiler/compile-structs.arr +++ b/src/arr/compiler/compile-structs.arr @@ -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: @@ -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: @@ -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: diff --git a/src/arr/compiler/desugar.arr b/src/arr/compiler/desugar.arr index 93042cbc93..0320ccf9a9 100644 --- a/src/arr/compiler/desugar.arr +++ b/src/arr/compiler/desugar.arr @@ -93,7 +93,7 @@ fun desugar-ann(a :: A.Ann) -> A.Ann: cases(A.Ann) a: | a-blank => a | a-any(_) => a - | a-name(_, _) => a + | a-name(l, _) => a | a-type-var(_, _) => a | a-dot(_, _, _) => a | a-arrow(l, args, ret, use-parens) => @@ -108,6 +108,8 @@ fun desugar-ann(a :: A.Ann) -> A.Ann: A.a-record(l, fields.map(desugar-afield)) | a-tuple(l, fields) => A.a-tuple(l, fields.map(desugar-ann)) + | a-unit(l, ann, u) => + A.a-unit(l, desugar-ann(ann), u) | a-pred(l, ann, exp) => A.a-pred(l, desugar-ann(ann), desugar-expr(exp)) end @@ -528,11 +530,11 @@ fun desugar-expr(expr :: A.Expr): | s-id-var(l, x) => expr | s-id-letrec(_, _, _) => expr | s-srcloc(_, _) => expr - | s-num(_, _) => expr + | s-num(_, _, _) => expr # num, den are exact ints, and s-frac desugars to the exact rational num/den - | s-frac(l, num, den) => A.s-num(l, num / den) # NOTE: Possibly must preserve further? + | s-frac(l, num, den, u) => A.s-num(l, num / den, u) # NOTE: Possibly must preserve further? # num, den are exact ints, and s-rfrac desugars to the roughnum fraction corresponding to num/den - | s-rfrac(l, num, den) => A.s-num(l, num-to-roughnum(num / den)) # NOTE: Possibly must preserve further? + | s-rfrac(l, num, den, u) => A.s-num(l, num-to-roughnum(num / den), u) # NOTE: Possibly must preserve further? | s-str(_, _) => expr | s-bool(_, _) => expr | s-obj(l, fields) => A.s-obj(l, fields.map(desugar-member)) @@ -958,8 +960,8 @@ where: p = lam(str): PP.surface-parse(str, "test").block.visit(A.dummy-loc-visitor) end ds = lam(prog): desugar-expr(prog).visit(unglobal).visit(A.dummy-loc-visitor) end id = lam(s): A.s-id(d, A.s-name(d, s)) end - one = A.s-num(d, 1) - two = A.s-num(d, 2) + one = A.s-num(d, 1, A.u-one(d)) + two = A.s-num(d, 2, A.u-one(d)) pretty = lam(prog): prog.tosource().pretty(80).join-str("\n") end if-else = "if true: 5 else: 6 end" diff --git a/src/arr/compiler/flatness.arr b/src/arr/compiler/flatness.arr index fa879e93f5..ac429983f8 100644 --- a/src/arr/compiler/flatness.arr +++ b/src/arr/compiler/flatness.arr @@ -69,6 +69,7 @@ fun ann-flatness(ann :: A.Ann, val-env :: FEnv, ann-env :: FEnv) -> Flatness: ann-flatness(base, val-env, ann-env), val-flatness ) + | a-unit(l, base, u) => ann-flatness(base, val-env, ann-env) | a-dot(l, obj, field) => none # TODO(joe): module-ids should make this doable... | a-checked(checked, residual) => none end diff --git a/src/arr/compiler/resolve-scope.arr b/src/arr/compiler/resolve-scope.arr index 743bfbbd59..a19341e711 100644 --- a/src/arr/compiler/resolve-scope.arr +++ b/src/arr/compiler/resolve-scope.arr @@ -470,29 +470,30 @@ where: thunk = lam(e): A.s-lam(d, "", [list: ], [list: ], A.a-blank, "", bk(e), n, n, false) end + u = A.u-one(d) compare1 = - A.s-let-expr(d, [list: A.s-let-bind(d, b("x"), A.s-num(d, 15)), - A.s-let-bind(d, b("y"), A.s-num(d, 10))], + A.s-let-expr(d, [list: A.s-let-bind(d, b("x"), A.s-num(d, 15, u)), + A.s-let-bind(d, b("y"), A.s-num(d, 10, u))], id("y"), false) dsb(p("x = 15 y = 10 y").stmts).visit(A.dummy-loc-visitor) is compare1 dsb(p("x = 55 var y = 10 y").stmts).visit(A.dummy-loc-visitor) - is A.s-let-expr(d, [list: A.s-let-bind(d, b("x"), A.s-num(d, 55)), - A.s-var-bind(d, b("y"), A.s-num(d, 10))], id("y"), false) + is A.s-let-expr(d, [list: A.s-let-bind(d, b("x"), A.s-num(d, 55, u)), + A.s-var-bind(d, b("y"), A.s-num(d, 10, u))], id("y"), false) bs("x = 7 print(2) var y = 10 y") is - A.s-let-expr(d, [list:A.s-let-bind(d, b("x"), A.s-num(d, 7))], + A.s-let-expr(d, [list:A.s-let-bind(d, b("x"), A.s-num(d, 7, u))], A.s-block(d, [list: - A.s-app(d, id("print"), [list:A.s-num(d, 2)]), - A.s-let-expr(d, [list:A.s-var-bind(d, b("y"), A.s-num(d, 10))], + A.s-app(d, id("print"), [list:A.s-num(d, 2, u)]), + A.s-let-expr(d, [list:A.s-var-bind(d, b("y"), A.s-num(d, 10, u))], id("y"), false) ]), false) prog = bs("fun f(): 4 end fun g(): 5 end f()") prog is A.s-letrec(d, [list: - A.s-letrec-bind(d, b("f"), thunk(A.s-num(d, 4))), - A.s-letrec-bind(d, b("g"), thunk(A.s-num(d, 5))) + A.s-letrec-bind(d, b("f"), thunk(A.s-num(d, 4, u))), + A.s-letrec-bind(d, b("g"), thunk(A.s-num(d, 5, u))) ], A.s-app(d, id("f"), [list: ]), false) @@ -501,13 +502,13 @@ where: prog2 = bs("print(1) fun f(): 4 end fun g(): 5 end fun h(): 6 end x = 3 print(x)") prog2 is A.s-block(d, - [list: p-s(A.s-num(d, 1)), + [list: p-s(A.s-num(d, 1, u)), A.s-letrec(d, [list: - A.s-letrec-bind(d, b("f"), thunk(A.s-num(d, 4))), - A.s-letrec-bind(d, b("g"), thunk(A.s-num(d, 5))), - A.s-letrec-bind(d, b("h"), thunk(A.s-num(d, 6))) + A.s-letrec-bind(d, b("f"), thunk(A.s-num(d, 4, u))), + A.s-letrec-bind(d, b("g"), thunk(A.s-num(d, 5, u))), + A.s-letrec-bind(d, b("h"), thunk(A.s-num(d, 6, u))) ], - A.s-let-expr(d, [list: A.s-let-bind(d, b("x"), A.s-num(d, 3))], p-s(id("x")), false), + A.s-let-expr(d, [list: A.s-let-bind(d, b("x"), A.s-num(d, 3, u))], p-s(id("x")), false), false)]) dsb([list: prog2]) is prog2 @@ -519,17 +520,17 @@ where: prog3 is A.s-block(d, [list: p-s(id("x")), - A.s-assign(d, A.s-name(d, "x"), A.s-num(d, 3)), + A.s-assign(d, A.s-name(d, "x"), A.s-num(d, 3, u)), p-s(id("x")) ]) prog4 = bs("var x = 10 fun f(): 4 end f()") prog4 is A.s-let-expr(d, [list: - A.s-var-bind(d, b("x"), A.s-num(d, 10)) + A.s-var-bind(d, b("x"), A.s-num(d, 10, u)) ], A.s-letrec(d, [list: - A.s-letrec-bind(d, b("f"), thunk(A.s-num(d, 4))) + A.s-letrec-bind(d, b("f"), thunk(A.s-num(d, 4, u))) ], A.s-app(d, id("f"), [list: ]), false), false @@ -707,7 +708,7 @@ where: ds = lam(prog): desugar-scope(prog, C.no-builtins).ast.visit(A.dummy-loc-visitor) end compare1 = A.s-program(d, A.s-provide-none(d), A.s-provide-types-none(d), [list: ], A.s-let-expr(d, [list: - A.s-let-bind(d, b("x"), A.s-num(d, 10)) + A.s-let-bind(d, b("x"), A.s-num(d, 10, A.u-one(d))) ], A.s-module(d, id("nothing"), empty, empty, id("x"), [list:], checks), false) ) @@ -1325,6 +1326,7 @@ fun resolve-names(p :: A.Program, initial-env :: C.CompileEnvironment): method a-record(self, l, fields): A.a-record(l, fields.map(_.visit(self))) end, method a-app(self, l, ann, args): A.a-app(l, ann.visit(self), args.map(_.visit(self))) end, method a-pred(self, l, ann, exp): A.a-pred(l, ann.visit(self), exp.visit(self)) end, + method a-unit(self, l, ann, u): A.a-unit(l, ann.visit(self), u) end, method a-dot(self, l, obj, field) block: cases(A.Name) obj block: | s-name(nameloc, s) => diff --git a/src/arr/compiler/type-check.arr b/src/arr/compiler/type-check.arr index 7cc04f33ce..d091f10e37 100644 --- a/src/arr/compiler/type-check.arr +++ b/src/arr/compiler/type-check.arr @@ -566,11 +566,11 @@ fun _checking(e :: Expr, expect-type :: Type, top-level :: Boolean, context :: C raise("checking for s-undefined not implemented") | s-srcloc(l, loc) => check-synthesis(e, expect-type, top-level, context) - | s-num(l, n) => + | s-num(l, n, u) => check-synthesis(e, expect-type, top-level, context) - | s-frac(l, num, den) => + | s-frac(l, num, den, u) => check-synthesis(e, expect-type, top-level, context) - | s-rfrac(l, num, den) => + | s-rfrac(l, num, den, u) => check-synthesis(e, expect-type, top-level, context) | s-bool(l, b) => check-synthesis(e, expect-type, top-level, context) @@ -828,11 +828,11 @@ fun _synthesis(e :: Expr, top-level :: Boolean, context :: Context) -> TypingRes raise("synthesis for s-undefined not implemented") | s-srcloc(l, loc) => typing-result(e, t-srcloc(l), context) - | s-num(l, n) => + | s-num(l, n, u) => typing-result(e, t-number(l), context) - | s-frac(l, num, den) => + | s-frac(l, num, den, u) => typing-result(e, t-number(l), context) - | s-rfrac(l, num, den) => + | s-rfrac(l, num, den, u) => typing-result(e, t-number(l), context) | s-bool(l, b) => typing-result(e, t-boolean(l), context) @@ -2295,6 +2295,9 @@ fun to-type(in-ann :: A.Ann, context :: Context) -> FoldResult>: fold-errors([list: C.cant-typecheck("missing annotation on " + tostring(ann), l)]) end end) + | a-unit(l, ann, u) => + # TODO(benmusch): Change this to tc units + to-type(ann, context) | a-dot(l, obj, field) => key = obj.key() origin = context.module-names.get(key) diff --git a/src/arr/compiler/well-formed.arr b/src/arr/compiler/well-formed.arr index afeacbb60d..a5539dfed9 100644 --- a/src/arr/compiler/well-formed.arr +++ b/src/arr/compiler/well-formed.arr @@ -292,6 +292,34 @@ fun wf-last-stmt(block-loc, stmt :: A.Expr): end end + +fun unit-opname(u :: A.Unit): + cases(A.Unit) u: + | u-mul(_, _, _, _) => "*" + | u-div(_, _, _, _) => "/" + end +end +fun reachable-ops-unit(self, l, op-l, parent, u): + cases(A.Unit) u block: + | u-mul(l2, op-l2, lhs, rhs) => + if A.is-u-mul(parent) block: + reachable-ops-unit(self, l2, op-l2, u, lhs) + reachable-ops-unit(self, l2, op-l2, u, lhs) + else: + add-error(C.mixed-unit-ops(l, unit-opname(parent), op-l, unit-opname(u), op-l2)) + end + | u-div(l2, op-l2, lhs, rhs) => + if A.is-u-div(parent) block: + reachable-ops-unit(self, l2, op-l2, u, lhs) + reachable-ops-unit(self, l2, op-l2, u, lhs) + else: + add-error(C.mixed-unit-ops(l, unit-opname(parent), op-l, unit-opname(u), op-l2)) + end + | else => u.visit(self) + end +end + + fun fields-to-binds(members :: List) -> List: for map(mem from members): A.s-bind(mem.l, false, A.s-name(mem.l, mem.name), A.a-blank) @@ -347,9 +375,9 @@ fun reject-standalone-exprs(stmts :: List%(is-link), ignore-last :: Boolean) blo ED.text(" operator expression probably isn't intentional.")]], l) end | s-id(_, _) => wf-error([list: [ED.para: ED.text("A standalone variable name probably isn't intentional.")]], l) - | s-num(_, _) => wf-error([list: [ED.para: ED.text("A standalone value probably isn't intentional.")]], l) - | s-frac(_, _, _) => wf-error([list: [ED.para: ED.text("A standalone value probably isn't intentional.")]], l) - | s-rfrac(_, _, _) => wf-error([list: [ED.para: ED.text("A standalone value probably isn't intentional.")]], l) + | s-num(_, _, _) => wf-error([list: [ED.para: ED.text("A standalone value probably isn't intentional.")]], l) + | s-frac(_, _, _, _) => wf-error([list: [ED.para: ED.text("A standalone value probably isn't intentional.")]], l) + | s-rfrac(_, _, _, _) => wf-error([list: [ED.para: ED.text("A standalone value probably isn't intentional.")]], l) | s-bool(_, _) => wf-error([list: [ED.para: ED.text("A standalone value probably isn't intentional.")]], l) | s-str(_, _) => wf-error([list: [ED.para: ED.text("A standalone value probably isn't intentional.")]], l) | s-dot(_, _, _) => wf-error([list: [ED.para: ED.text("A standalone field-lookup expression probably isn't intentional.")]], l) @@ -857,16 +885,20 @@ well-formed-visitor = A.default-iter-visitor.{ end iterator.visit(self) and lists.all(_.visit(self), bindings) and ann.visit(self) and body.visit(self) end, - method s-frac(self, l, num, den) block: + method s-frac(self, l, num, den, u) block: when den == 0: add-error(C.zero-fraction(l, num)) end + + u.visit(self) true end, - method s-rfrac(self, l, num, den) block: + method s-rfrac(self, l, num, den, u) block: when den == 0: add-error(C.zero-fraction(l, num)) end + + u.visit(self) true end, method s-id(self, l, id) block: @@ -989,6 +1021,41 @@ well-formed-visitor = A.default-iter-visitor.{ add-error(C.underscore-as-ann(l)) end true + end, + method a-unit(self, l, base, u) block: + cases(A.Unit) u: + | u-base(_, id) => when not(A.is-s-underscore(id)): u.visit(self) end + | else => u.visit(self) + end + base.visit(self) + true + end, + method u-mul(self, l :: Loc, op-l :: Loc, lhs :: A.Unit, rhs :: A.Unit) block: + reachable-ops-unit(self, l, op-l, A.u-mul(l, op-l, lhs, rhs), lhs) + reachable-ops-unit(self, l, op-l, A.u-mul(l, op-l, lhs, rhs), rhs) + true + end, + method u-div(self, l :: Loc, op-l :: Loc, lhs :: A.Unit, rhs :: A.Unit) block: + reachable-ops-unit(self, l, op-l, A.u-div(l, op-l, lhs, rhs), lhs) + reachable-ops-unit(self, l, op-l, A.u-div(l, op-l, lhs, rhs), rhs) + true + end, + method u-pow(self, l :: Loc, op-l :: Loc, u :: A.Unit, n :: Number) block: + when (n == 0) or not(num-is-integer(n)): + add-error(C.invalid-unit-power(l, n)) + end + + when A.is-u-one(u): + add-error(C.one-as-power-base(l, n)) + end + u.visit(self) + true + end, + method u-base(self, l :: Loc, id :: A.Name) block: + when A.is-s-underscore(id): + add-error(C.underscore-as-unit(l)) + end + true end } @@ -1247,14 +1314,14 @@ top-level-visitor = A.default-iter-visitor.{ method s-prim-app(_, l :: Loc, _fun :: String, args :: List, app-info :: A.PrimAppInfo): well-formed-visitor.s-prim-app(l, _fun, args, app-info) end, - method s-frac(_, l :: Loc, num, den): - well-formed-visitor.s-frac(l, num, den) + method s-frac(_, l :: Loc, num, den, u): + well-formed-visitor.s-frac(l, num, den, u) end, method s-reactor(self, l, fields): well-formed-visitor.s-reactor(l, fields) end, - method s-rfrac(_, l :: Loc, num, den): - well-formed-visitor.s-rfrac(l, num, den) + method s-rfrac(_, l :: Loc, num, den, u): + well-formed-visitor.s-rfrac(l, num, den, u) end, method s-id(_, l :: Loc, id :: A.Name): well-formed-visitor.s-id(l, id) @@ -1307,6 +1374,9 @@ top-level-visitor = A.default-iter-visitor.{ method s-table-extend(_, l :: Loc, column-binds :: A.ColumnBinds, extensions :: List): well-formed-visitor.s-table-extend(l, column-binds, extensions) end, + method s-num(self, l, n, u): + well-formed-visitor.s-num(l, n, u) + end, method a-arrow(_, l, args, ret, use-parens): well-formed-visitor.a-arrow(l, args, ret, use-parens) end, @@ -1325,6 +1395,9 @@ top-level-visitor = A.default-iter-visitor.{ method a-pred(_, l, ann, exp): well-formed-visitor.a-pred(l, ann, exp) end, + method a-unit(_, l, ann, u): + well-formed-visitor.a-unit(l, ann, u) + end, method a-dot(_, l, obj, field): well-formed-visitor.a-dot(l, obj, field) end, diff --git a/src/arr/trove/ast.arr b/src/arr/trove/ast.arr index ef34d96d8e..9f7c7b6fa7 100644 --- a/src/arr/trove/ast.arr +++ b/src/arr/trove/ast.arr @@ -104,6 +104,9 @@ str-extract = PP.str("extract") str-load-table = PP.str("load-table:") str-src = PP.str("source:") str-sanitize = PP.str("sanitize") +str-one = PP.str("1") +str-times = PP.str("*") +str-divide = PP.str("/") data Name: | s-underscore(l :: Loc) with: @@ -137,7 +140,7 @@ data Name: method tosourcestring(self): "$type$" + self.s end, method toname(self): self.s end, method key(self): "tglobal#" + self.s end - + | s-atom(base :: String, serial :: Number) with: method to-compiled-source(self): PP.str(self.to-compiled()) end, method to-compiled(self): self.base + tostring(self.serial) end, @@ -928,15 +931,38 @@ data Expr: | s-srcloc(l :: Loc, loc :: Loc) with: method label(self): "s-srcloc" end, method tosource(self): PP.str(torepr(self.loc)) end - | s-num(l :: Loc, n :: Number) with: + | s-num(l :: Loc, n :: Number, u :: Unit) with: method label(self): "s-num" end, - method tosource(self): PP.number(self.n) end - | s-frac(l :: Loc, num :: NumInteger, den :: NumInteger) with: + method tosource(self): + if 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 + | s-frac(l :: Loc, num :: NumInteger, den :: NumInteger, u :: Unit) with: method label(self): "s-frac" end, - method tosource(self): PP.number(self.num) + PP.str("/") + PP.number(self.den) end - | s-rfrac(l :: Loc, num :: NumInteger, den :: NumInteger) with: + method tosource(self) block: + base-str = PP.number(self.num) + PP.str("/") + PP.number(self.den) + if is-u-one(self.u): + base-str + else: + PP.separate(str-percent, + [list: base-str, PP.surround(INDENT, 0, PP.langle, self.u.tosource(), PP.rangle)]) + end + end + | s-rfrac(l :: Loc, num :: NumInteger, den :: NumInteger, u :: Unit) with: method label(self): "s-rfrac" end, - method tosource(self): PP.str("~") + PP.number(self.num) + PP.str("/") + PP.number(self.den) end + method tosource(self): + base-str = PP.str("~") + PP.number(self.num) + PP.str("/") + PP.number(self.den) + if is-u-one(self.u): + base-str + else: + PP.separate(str-percent, + [list: base-str, PP.surround(INDENT, 0, PP.langle, self.u.tosource(), PP.rangle)]) + end + end | s-bool(l :: Loc, b :: Boolean) with: method label(self): "s-bool" end, method tosource(self): PP.str(tostring(self.b)) end @@ -1625,6 +1651,38 @@ sharing: end end +data Unit: + | u-one(l :: Loc) with: + method label(self): "u-one" end, + method tosource(self): str-one end + | u-base(l :: Loc, id :: Name) with: + method label(self): "u-base" end, + method tosource(self): self.id.tosource() end + | u-mul(l :: Loc, op-l :: Loc, lhs :: Unit, rhs :: Unit) with: + method label(self): "u-mul" end, + method tosource(self): + PP.separate(str-space, [list: self.lhs.tosource(), str-times, self.rhs.tosource()]) + end + | u-div(l :: Loc, op-l :: Loc, lhs :: Unit, rhs :: Unit) with: + method label(self): "u-div" end, + method tosource(self): + PP.separate(str-space, [list: self.lhs.tosource(), str-divide, self.rhs.tosource()]) + end + | u-pow(l :: Loc, op-l :: Loc, u :: Unit, n :: Number) with: + method label(self): "u-pow" end, + method tosource(self): + PP.separate(str-space, [list: self.u.tosource(), str-caret, PP.number(self.n)]) + end + | u-paren(l :: Loc, u :: Unit) with: + method label(self): "u-paren" end, + method tosource(self): PP.paren(self.u.tosource()) end +sharing: + method visit(self, visitor): + self._match(visitor, lam(val): raise("No visitor field for " + self.label()) end) + end +end + + data Ann: | a-blank with: method label(self): "a-blank" end, @@ -1691,6 +1749,12 @@ data Ann: | a-pred(l :: Loc, ann :: Ann, exp :: Expr) with: method label(self): "a-pred" end, method tosource(self): self.ann.tosource() + str-percent + PP.parens(self.exp.tosource()) end, + | a-unit(l :: Loc, ann :: Ann, u :: Unit) with: + method label(self): "a-unit" end, + method tosource(self) block: + unit-str = PP.surround(INDENT, 0, PP.langle, self.u.tosource(), PP.rangle) + self.ann.tosource() + str-percent + unit-str + end | a-dot(l :: Loc, obj :: Name, field :: String) with: method label(self): "a-dot" end, method tosource(self): self.obj.tosource() + PP.str("." + self.field) end, @@ -2130,14 +2194,14 @@ default-map-visitor = { method s-srcloc(self, l, shadow loc): s-srcloc(l, loc) end, - method s-num(self, l :: Loc, n :: Number): - s-num(l, n) + method s-num(self, l :: Loc, n :: Number, u :: Unit): + s-num(l, n, u.visit(self)) end, - method s-frac(self, l :: Loc, num :: NumInteger, den :: NumInteger): - s-frac(l, num, den) + method s-frac(self, l :: Loc, num :: NumInteger, den :: NumInteger, u :: Unit): + s-frac(l, num, den, u.visit(self)) end, - method s-rfrac(self, l :: Loc, num :: NumInteger, den :: NumInteger): - s-rfrac(l, num, den) + method s-rfrac(self, l :: Loc, num :: NumInteger, den :: NumInteger, u :: Unit): + s-rfrac(l, num, den, u.visit(self)) end, method s-bool(self, l :: Loc, b :: Boolean): s-bool(l, b) @@ -2342,11 +2406,32 @@ default-map-visitor = { method a-pred(self, l, ann, exp): a-pred(l, ann.visit(self), exp.visit(self)) end, + method a-unit(self, l, ann, u): + a-unit(l, ann.visit(self), u.visit(self)) + end, method a-dot(self, l, obj, field): a-dot(l, obj.visit(self), field) end, method a-field(self, l, name, ann): a-field(l, name, ann.visit(self)) + end, + method u-one(self, l): + u-one(l) + end, + method u-base(self, l, id): + u-base(l, id) + end, + method u-mul(self, l :: Loc, op-l :: Loc, lhs :: Unit, rhs :: Unit): + u-mul(l, op-l, lhs.visit(self), rhs.visit(self)) + end, + method u-div(self, l :: Loc, op-l :: Loc, lhs :: Unit, rhs :: Unit): + u-div(l, op-l, lhs.visit(self), rhs.visit(self)) + end, + method u-pow(self, l :: Loc, op-l :: Loc, u :: Unit, n :: Number): + u-pow(l, op-l, u.visit(self), n) + end, + method u-paren(self, l :: Loc, u :: Unit): + u-paren(l, u.visit(self)) end } @@ -2690,14 +2775,14 @@ default-iter-visitor = { method s-srcloc(self, l, shadow loc): true end, - method s-num(self, l :: Loc, n :: Number): - true + method s-num(self, l :: Loc, n :: Number, u :: Unit): + u.visit(self) end, - method s-frac(self, l :: Loc, num :: NumInteger, den :: NumInteger): - true + method s-frac(self, l :: Loc, num :: NumInteger, den :: NumInteger, u :: Unit): + u.visit(self) end, - method s-rfrac(self, l :: Loc, num :: NumInteger, den :: NumInteger): - true + method s-rfrac(self, l :: Loc, num :: NumInteger, den :: NumInteger, u :: Unit): + u.visit(self) end, method s-bool(self, l :: Loc, b :: Boolean): true @@ -2892,11 +2977,32 @@ default-iter-visitor = { method a-pred(self, l, ann, exp): ann.visit(self) and exp.visit(self) end, + method a-unit(self, l, ann, u): + ann.visit(self) and u.visit(self) + end, method a-dot(self, l, obj, field): obj.visit(self) end, method a-field(self, l, name, ann): ann.visit(self) + end, + method u-one(self, l): + true + end, + method u-base(self, l, id): + true + end, + method u-mul(self, l :: Loc, op-l :: Loc, lhs :: Unit, rhs :: Unit): + lhs.visit(self) and rhs.visit(self) + end, + method u-div(self, l :: Loc, op-l :: Loc, lhs :: Unit, rhs :: Unit): + lhs.visit(self) and rhs.visit(self) + end, + method u-pow(self, l :: Loc, op-l :: Loc, u :: Unit, n :: Number): + u.visit(self) + end, + method u-paren(self, l :: Loc, u :: Unit): + u.visit(self) end } @@ -3227,14 +3333,14 @@ dummy-loc-visitor = { method s-srcloc(self, l, shadow loc): s-srcloc(dummy-loc, loc) end, - method s-num(self, l :: Loc, n :: Number): - s-num(dummy-loc, n) + method s-num(self, l :: Loc, n :: Number, u :: Unit): + s-num(dummy-loc, n, u.visit(self)) end, - method s-frac(self, l :: Loc, num :: NumInteger, den :: NumInteger): - s-frac(dummy-loc, num, den) + method s-frac(self, l :: Loc, num :: NumInteger, den :: NumInteger, u :: Unit): + s-frac(dummy-loc, num, den, u.visit(self)) end, - method s-rfrac(self, l :: Loc, num :: NumInteger, den :: NumInteger): - s-rfrac(dummy-loc, num, den) + method s-rfrac(self, l :: Loc, num :: NumInteger, den :: NumInteger, u :: Unit): + s-rfrac(dummy-loc, num, den, u.visit(self)) end, method s-bool(self, l :: Loc, b :: Boolean): s-bool(dummy-loc, b) @@ -3439,10 +3545,27 @@ dummy-loc-visitor = { method a-pred(self, l, ann, exp): a-pred(dummy-loc, ann.visit(self), exp.visit(self)) end, + method a-unit(self, l, ann, u): + a-unit(dummy-loc, ann.visit(self), u.visit(self)) + end, method a-dot(self, l, obj, field): a-dot(dummy-loc, obj, field) end, method a-field(self, l, name, ann): a-field(dummy-loc, name, ann.visit(self)) + end, + method u-one(self, l): u-one(dummy-loc) end, + method u-base(self, l, id): u-base(dummy-loc, id) end, + method u-mul(self, l :: Loc, op-l :: Loc, lhs :: Unit, rhs :: Unit): + u-mul(dummy-loc, dummy-loc, lhs.visit(self), rhs.visit(self)) + end, + method u-div(self, l :: Loc, op-l :: Loc, lhs :: Unit, rhs :: Unit): + u-div(dummy-loc, dummy-loc, lhs.visit(self), rhs.visit(self)) + end, + method u-pow(self, l :: Loc, op-l :: Loc, u :: Unit, n :: Number): + u-pow(dummy-loc, dummy-loc, u.visit(self), n) + end, + method u-paren(self, l :: Loc, u :: Unit): + u-paren(dummy-loc, u.visit(self)) end } diff --git a/src/arr/trove/contracts.arr b/src/arr/trove/contracts.arr index bcd5c1d15e..8534752806 100644 --- a/src/arr/trove/contracts.arr +++ b/src/arr/trove/contracts.arr @@ -294,6 +294,31 @@ data FailureReason: [ED.error: message, ED.embed(self.val)] end end + | unit-fail(val, name, expected, actual, is-implicit) with: + method render-fancy-reason(self, loc, from-fail-arg, maybe-stack-loc, src-available, maybe-ast): + self.render-reason(loc, from-fail-arg) + end, + method render-reason(self, loc, from-fail-arg): + message = [ED.para: + ED.text("The predicate "), ED.code(ED.text(self.name)), + ED.text(" in the annotation at "), draw-and-highlight(loc), + ED.text(" returned false for this value: "), ED.embed(self.val)] + if self.is-implicit and not(loc.is-builtin()): + [ED.error: + message, + [ED.para: + ED.text("The unit "), ED.code(ED.text(self.actual)), + ED.text(" was expected to match "), ED.code(ED.text(self.expected)), + ED.text(" due to the default of the "), ED.code(ED.text(self.name)), + ED.text(" annotation. Did you mean to specify another unit or the any unit (%<_>)?")]] + else: + [ED.error: + message, + [ED.para: + ED.text("The unit "), ED.code(ED.text(self.actual)), + ED.text(" was expected to match "), ED.code(ED.text(self.expected))]] + end + end | record-fields-fail(val, field-failures :: List) with: method render-fancy-reason(self, loc, from-fail-arg, maybe-stack-loc, src-available, maybe-ast): [ED.error: diff --git a/src/arr/trove/error.arr b/src/arr/trove/error.arr index 6db43a79fd..734d66fbcc 100644 --- a/src/arr/trove/error.arr +++ b/src/arr/trove/error.arr @@ -2428,6 +2428,177 @@ data RuntimeError: method render-reason(self): ED.text("") end + | units-on-unsupported-ann(loc, unit-str) with: + method render-fancy-reason(self, maybe-stack-loc, src-available, maybe-ast): + if self.loc.is-builtin(): + [ED.error: + ed-intro("unit annotation", self.loc, -1, true), + ED.cmcode(self.loc), + [ED.para: ED.text("It annotated a type which does not support units.")], + please-report-bug()] + else if src-available(self.loc): + cases(O.Option) maybe-ast(self.loc): + | some(ast) => + [ED.error: + ed-intro("unit annotation", self.loc, -1, true), + ED.cmcode(self.loc), + [ED.para: + ED.highlight(ED.text("The unit " + self.unit-str), [ED.locs: ast.l], 0), + ED.text(" annotated a "), + ED.highlight(ED.text("base type"), [ED.locs: ast.ann.l], 1), + ED.text(" which does not support units. Consider removing the unit annotation or changing the base type.")]] + | none => + [ED.error: + ed-intro("unit annotation", self.loc, -1, true), + ED.cmcode(self.loc), + [ED.para: + ED.text("It annotated a type which does not support units. "), + ED.text("Consider removing the unit annotation or changing the base type.")]] + end + else: + self.render-reason() + end + end, + method render-reason(self) block: + base-err = [ED.para: + ed-simple-intro("unit annotation", self.loc), + ED.text("The annotation is annotated with the unit "), + ED.code(ED.text(self.unit-str)), + ED.text(" but does not support unit annotations.")] + if self.loc.is-builtin(): + [ED.error: base-err, please-report-bug()] + else: + [ED.error: base-err] + end + end + | incompatible-units(reason, u, v) with: + method render-fancy-reason(self, maybe-stack-loc, src-available, maybe-ast) block: + base-err-msg = [ED.para: + ED.text("The units "), + ED.code(ED.text(self.u)), + ED.text(" and "), + ED.code(ED.text(self.v)), + ED.text(" are not compatible")] + [ED.error: + cases(O.Option) maybe-stack-loc(0, true) block: + | some(loc) => + default-err-msg = [ED.sequence: + ed-intro(self.reason, loc, -1, true), + ED.cmcode(loc), + base-err-msg] + if loc.is-builtin(): + [ED.sequence: + ed-intro(self.reason, loc, -1, true), + base-err-msg, + please-report-bug()] + else if src-available(loc): + cases(O.Option) maybe-ast(loc): + | some(ast) => + cases(Any) ast: + | s-op(_l, op-l, opname, l, r) => + left-loc = l.l + right-loc = r.l + [ED.sequence: + ed-intro(self.reason, loc, -1, true), + ED.cmcode(loc), + [ED.para: + ED.text("The "), + ED.highlight(ED.text("left side"), [ED.locs: left-loc], 0), + ED.text(" had the unit:")], + ED.code(ED.text(self.u)), + [ED.para: + ED.text("The "), + ED.highlight(ED.text("right side"), [ED.locs: right-loc], 1), + ED.text(" had the unit: ")], + ED.code(ED.text(self.v)), + [ED.para: ED.text("These units are not compatible")]] + | else => default-err-msg + end + | none => default-err-msg + end + else: + base-err-msg + end + | none => + [ED.sequence: + [ED.para: + ED.text("A "), + ED.code(ED.text(self.reason)), + ED.text(" errored.")], + base-err-msg] + end] + end, + method render-reason(self) block: + base-err-msg = [ED.para: + ED.text("The units "), + ED.code(ED.text(self.u)), + ED.text(" and "), + ED.code(ED.text(self.v)), + ED.text(" are not compatible")] + [ED.error: ED.maybe-stack-loc(0, false, + lam(loc): + [ED.sequence: + ed-simple-intro(self.reason, loc), + base-err-msg] + end, + [ED.sequence: + [ED.para: + ED.text("A "), + ED.code(ED.text(self.reason)), + ED.text(" errored.")], + base-err-msg])] + end + | invalid-unit-state(opname, n, desc) with: + method render-fancy-reason(self, maybe-stack-loc, src-available, maybe-ast): + base-err-msg = [ED.sequence: + [ED.para: + ED.text("The " + self.opname + " function failed.")], + [ED.para: + ED.code(ED.text(tostring(self.n))), + ED.text(" is an invalid argument because: "), + ED.text(self.desc)]] + [ED.error: + cases(O.Option) maybe-stack-loc(0, false): + | some(loc) => + if loc.is-builtin(): + [ED.sequence: + ed-intro(self.opname + " function", loc, -1, true), + [ED.para: + ED.code(ED.text(tostring(self.n))), + ED.text(" is an invalid argument because: "), + ED.text(self.desc)], + please-report-bug()] + else if src-available(loc): + [ED.sequence: + ed-intro(self.opname + " function", loc, -1, true), + ED.cmcode(loc), + [ED.para: + ED.code(ED.text(tostring(self.n))), + ED.text(" is an invalid argument because: "), + ED.text(self.desc)]] + else: + base-err-msg + end + | none => base-err-msg + end] + end, + method render-reason(self): + [ED.error: ED.maybe-stack-loc(0, false, + lam(loc): + [ED.sequence: + ed-simple-intro(self.opname + " function", loc), + [ED.para: + ED.code(ED.text(tostring(self.n))), + ED.text(" is an invalid argument because: "), + ED.text(self.desc)]] + end, + [ED.sequence: + [ED.para: ED.text("The " + self.opname + " function failed.")], + [ED.para: + ED.code(ED.text(tostring(self.n))), + ED.text(" is an invalid argument because: "), + ED.text(self.desc)]])] + end end data ParseError: diff --git a/src/js/base/js-numbers.js b/src/js/base/js-numbers.js index d89c0b96ee..6fb647b996 100644 --- a/src/js/base/js-numbers.js +++ b/src/js/base/js-numbers.js @@ -98,7 +98,7 @@ pyretnum := fixnum | boxnum A fixnum is simply a JS double, and we prefer to use them whenever possible, viz., for integers that are small enough. -boxnum := BigInteger | Rational | Roughnum. +boxnum := BigInteger | Rational | Roughnum | Unitnum. An integer is either a fixnum or a BigInteger. @@ -108,6 +108,8 @@ define("pyret-base/js/js-numbers", function() { 'use strict'; // Abbreviation var Numbers = {}; + var UNIT_ONE = { "$name": "ONE" }; + var UNIT_ANY = { "$name": "ANY" }; // makeNumericBinop: (fixnum fixnum -> any) (pyretnum pyretnum -> any) -> (pyretnum pyretnum) X // Creates a binary function that works either on fixnums or boxnums. @@ -116,7 +118,6 @@ define("pyret-base/js/js-numbers", function() { var makeNumericBinop = function(onFixnums, onBoxednums, options) { options = options || {}; return function(x, y, errbacks) { - if (options.isXSpecialCase && options.isXSpecialCase(x, errbacks)) return options.onXSpecialCase(x, y, errbacks); if (options.isYSpecialCase && options.isYSpecialCase(y, errbacks)) @@ -134,7 +135,11 @@ define("pyret-base/js/js-numbers", function() { y = liftFixnumInteger(y, x); } - if (x instanceof Roughnum) { + if (x instanceof Unitnum || y instanceof Unitnum) { + // if x or y have units, ensure they are both wrapped in unitnums + x = _withUnit(x, getUnit(x), true); + y = _withUnit(y, getUnit(y), true); + } else if (x instanceof Roughnum) { // y is rough, rat or bigint if (!(y instanceof Roughnum)) { // y is rat or bigint @@ -240,7 +245,8 @@ define("pyret-base/js/js-numbers", function() { return (typeof(thing) === 'number' || (thing instanceof Rational || thing instanceof Roughnum || - thing instanceof BigInteger)); + thing instanceof BigInteger || + thing instanceof Unitnum)); }; // isRational: pyretnum -> boolean @@ -249,6 +255,11 @@ define("pyret-base/js/js-numbers", function() { (isPyretNumber(n) && n.isRational())); }; + // isUnitnum: pyretnum -> boolean + var isUnitnum = function(n) { + return n instanceof Unitnum; + }; + var isExact = isRational; // isReal: pyretnum -> boolean @@ -356,14 +367,20 @@ define("pyret-base/js/js-numbers", function() { } }, function(x, y, errbacks) { - return x.add(y); + return x.add(y, errbacks); }, {isXSpecialCase: function(x, errbacks) { return isInteger(x) && _integerIsZero(x) }, - onXSpecialCase: function(x, y, errbacks) { return y; }, + onXSpecialCase: function(x, y, errbacks) { + ensureSameUnits(x, y, errbacks, "+ operation"); + return y; + }, isYSpecialCase: function(y, errbacks) { return isInteger(y) && _integerIsZero(y) }, - onYSpecialCase: function(x, y, errbacks) { return x; } + onYSpecialCase: function(x, y, errbacks) { + ensureSameUnits(x, y, errbacks, "+ operation"); + return x; + } }); var subtract = function(x, y, errbacks) { @@ -389,14 +406,20 @@ define("pyret-base/js/js-numbers", function() { } }, function(x, y, errbacks) { - return x.subtract(y); + return x.subtract(y, errbacks); }, {isXSpecialCase: function(x, errbacks) { return isInteger(x) && _integerIsZero(x) }, - onXSpecialCase: function(x, y, errbacks) { return negate(y, errbacks); }, + onXSpecialCase: function(x, y, errbacks) { + ensureSameUnits(x, y, errbacks, "- operation"); + return negate(y, errbacks); + }, isYSpecialCase: function(y, errbacks) { return isInteger(y) && _integerIsZero(y) }, - onYSpecialCase: function(x, y, errbacks) { return x; } + onYSpecialCase: function(x, y, errbacks) { + ensureSameUnits(x, y, errbacks, "- operation"); + return x; + } }); // mulitply: pyretnum pyretnum -> pyretnum @@ -425,7 +448,7 @@ define("pyret-base/js/js-numbers", function() { return x.multiply(y, errbacks); }, {isXSpecialCase: function(x, errbacks) { - return (isInteger(x) && + return (isInteger(x) && !(x instanceof Unitnum) && (_integerIsZero(x) || _integerIsOne(x) || _integerIsNegativeOne(x))) }, onXSpecialCase: function(x, y, errbacks) { if (_integerIsZero(x)) @@ -436,7 +459,7 @@ define("pyret-base/js/js-numbers", function() { return negate(y, errbacks); }, isYSpecialCase: function(y, errbacks) { - return (isInteger(y) && + return (isInteger(y) && !(y instanceof Unitnum) && (_integerIsZero(y) || _integerIsOne(y) || _integerIsNegativeOne(y)))}, onYSpecialCase: function(x, y, errbacks) { if (_integerIsZero(y)) @@ -452,7 +475,7 @@ define("pyret-base/js/js-numbers", function() { var divide = makeNumericBinop( function(x, y, errbacks) { if (_integerIsZero(y)) - errbacks.throwDivByZero("/: division by zero, " + x + ' ' + y); + errbacks.throwDivByZero("/: division by zero, " + x.toString() + ' ' + y.toString()); var div = x / y; if (isOverflow(div)) { return (makeBignum(x)).divide(makeBignum(y), errbacks); @@ -464,7 +487,7 @@ define("pyret-base/js/js-numbers", function() { }, function(x, y, errbacks) { if (equalsAnyZero(y, errbacks)) { - errbacks.throwDivByZero('/: division by zero, ' + x + ' ' + y); + errbacks.throwDivByZero("/: division by zero, " + x.toString() + ' ' + y.toString()); } return x.divide(y, errbacks); }, @@ -474,7 +497,7 @@ define("pyret-base/js/js-numbers", function() { }, onXSpecialCase: function(x, y, errbacks) { if (equalsAnyZero(y, errbacks)) { - errbacks.throwDivByZero("/: division by zero, " + x + ' ' + y); + errbacks.throwDivByZero("/: division by zero, " + x.toString() + ' ' + y.toString()); } return 0; }, @@ -482,7 +505,7 @@ define("pyret-base/js/js-numbers", function() { return equalsAnyZero(y, errbacks); }, onYSpecialCase: function(x, y, errbacks) { - errbacks.throwDivByZero("/: division by zero, " + x + ' ' + y); + errbacks.throwDivByZero("/: division by zero, " + x.toString() + ' ' + y.toString()); } }); @@ -507,7 +530,7 @@ define("pyret-base/js/js-numbers", function() { var equalsAnyZero = function(x, errbacks) { if (typeof(x) === 'number') return x === 0; if (isRoughnum(x)) return x.n === 0; - return x.equals(0, errbacks); + return equals(_withoutUnit(x), 0, errbacks); }; // eqv: pyretnum pyretnum -> boolean @@ -527,7 +550,10 @@ define("pyret-base/js/js-numbers", function() { }; // used for within - var roughlyEquals = function(x, y, delta, errbacks) { + var roughlyEquals = function(x, y, delta, where, errbacks) { + ensureSameUnits(x, y, errbacks, where + "'s arguments"); + ensureSameUnits(y, delta, errbacks, where + "'s tolerance"); + if (isNegative(delta)) { errbacks.throwToleranceError("negative tolerance " + delta); } @@ -536,7 +562,7 @@ define("pyret-base/js/js-numbers", function() { if (isRoughnum(delta) && delta.n === Number.MIN_VALUE) { if ((isRoughnum(x) || isRoughnum(y)) && - (Math.abs(subtract(x,y).n) === Number.MIN_VALUE)) { + (Math.abs(_withoutUnit(subtract(x,y)).n) === Number.MIN_VALUE)) { errbacks.throwToleranceError("roughnum tolerance too small for meaningful comparison, " + x + ' ' + y + ' ' + delta); } } @@ -548,7 +574,12 @@ define("pyret-base/js/js-numbers", function() { return approxEquals(ratx, raty, ratdelta, errbacks); }; - var roughlyEqualsRel = function(computedValue, trueValue, delta, errbacks) { + var roughlyEqualsRel = function(computedValue, trueValue, delta, where, errbacks) { + if (!checkUnit(getUnit(delta), UNIT_ONE)) { + errbacks.throwInvalidUnitState("relative rough equality", delta, "tolerance cannot have a unit"); + } + ensureSameUnits(computedValue, trueValue, errbacks, where + "'s arguments"); + if (isNegative(delta)) { errbacks.throwRelToleranceError('negative relative tolerance ' + delta) } @@ -597,7 +628,7 @@ define("pyret-base/js/js-numbers", function() { return x >= y; } return makeNumericBinop(undefined, function(x, y, errbacks) { - return x.greaterThanOrEqual(y); + return x.greaterThanOrEqual(y, errbacks); })(x, y, errbacks); } @@ -607,7 +638,7 @@ define("pyret-base/js/js-numbers", function() { return x <= y; } return makeNumericBinop(undefined, function(x, y, errbacks) { - return x.lessThanOrEqual(y); + return x.lessThanOrEqual(y, errbacks); })(x, y, errbacks); }; @@ -617,7 +648,7 @@ define("pyret-base/js/js-numbers", function() { return x > y; } return makeNumericBinop(undefined, function(x, y, errbacks) { - return x.greaterThan(y); + return x.greaterThan(y, errbacks); })(x, y, errbacks); }; @@ -627,7 +658,7 @@ define("pyret-base/js/js-numbers", function() { return x < y; } return makeNumericBinop(undefined, function(x, y, errbacks) { - return x.lessThan(y); + return x.lessThan(y, errbacks); })(x, y, errbacks); }; @@ -642,13 +673,21 @@ define("pyret-base/js/js-numbers", function() { } }, function(x, y, errbacks) { - return x.expt(y, errbacks); + if (!checkUnit(getUnit(y), UNIT_ONE)) { + errbacks.throwInvalidUnitState("num-expt", y, "power cannot have a unit") + } + return x.expt(_withoutUnit(y), errbacks); }, { isXSpecialCase: function(x, errbacks) { - return eqv(x, 0, errbacks) || eqv(x, 1, errbacks); + return (eqv(x, 0, errbacks) || eqv(x, 1, errbacks)); }, onXSpecialCase: function(x, y, errbacks) { + if (!checkUnit(getUnit(y), UNIT_ONE)) { + // need this because the isXSpecialCase check cannot look at the y + errbacks.throwInvalidUnitState("num-expt", y, "power cannot have a unit") + } + if (eqv(x, 0, errbacks)) { if (eqv(y, 0, errbacks)) { return 1; @@ -663,7 +702,7 @@ define("pyret-base/js/js-numbers", function() { }, isYSpecialCase: function(y, errbacks) { - return eqv(y, 0, errbacks) || lessThan(y, 0, errbacks); + return !(y instanceof Unitnum) && (eqv(y, 0, errbacks) || lessThan(y, 0, errbacks)); }, onYSpecialCase: function(x, y, errbacks) { if (eqv(y, 0, errbacks)) { @@ -736,19 +775,19 @@ define("pyret-base/js/js-numbers", function() { var numerator = function(n, errbacks) { if (typeof(n) === 'number') return n; - return n.numerator(); + return n.numerator(errbacks); }; // denominator: pyretnum -> pyretnum var denominator = function(n, errbacks) { if (typeof(n) === 'number') return 1; - return n.denominator(); + return n.denominator(errbacks); }; // sqrt: pyretnum -> pyretnum var sqrt = function(n, errbacks) { - if (lessThan(n, 0, errbacks)) { + if (isNegative(n)) { errbacks.throwSqrtNegative('sqrt: negative argument ' + n); } if (typeof(n) === 'number') { @@ -805,7 +844,7 @@ define("pyret-base/js/js-numbers", function() { if ( eqv(n, 1, errbacks) ) { return 0; } - if (lessThanOrEqual(n, 0, errbacks)) { + if (isNonPositive(n)) { errbacks.throwLogNonPositive('log: non-positive argument ' + n); } if (typeof(n) === 'number') { @@ -880,7 +919,7 @@ define("pyret-base/js/js-numbers", function() { // acos: pyretnum -> pyretnum var acos = function(n, errbacks) { if (eqv(n, 1, errbacks)) { return 0; } - if (lessThan(n, -1, errbacks) || greaterThan(n, 1, errbacks)) { + if (lessThan(_withoutUnit(n), -1, errbacks) || greaterThan(_withoutUnit(n), 1, errbacks)) { errbacks.throwDomainError('acos: out of domain argument ' + n); } if (typeof(n) === 'number') { @@ -892,7 +931,7 @@ define("pyret-base/js/js-numbers", function() { // asin: pyretnum -> pyretnum var asin = function(n, errbacks) { if (eqv(n, 0, errbacks)) { return 0; } - if (lessThan(n, -1, errbacks) || greaterThan(n, 1, errbacks)) { + if (lessThan(_withoutUnit(n), -1, errbacks) || greaterThan(_withoutUnit(n), 1, errbacks)) { errbacks.throwDomainError('asin: out of domain argument ' + n); } if (typeof(n) === 'number') { @@ -1046,6 +1085,181 @@ define("pyret-base/js/js-numbers", function() { ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// + // Unit operations + + var COUNT_FIELD = "$count"; + + var _withUnit = function(n, u, forceUnitnum) { + if (checkUnit(u, UNIT_ONE) && !forceUnitnum) { + return _withoutUnit(n); + } else if (n instanceof Unitnum) { + return _withUnit(n.n, u, forceUnitnum); + } else { + return new Unitnum(n, u); + } + } + + var _withoutUnit = function(n) { + if (n instanceof Unitnum) { + return _withoutUnit(n.n); + } else { + return n; + } + } + + var unitToString = function(u) { + if (u === UNIT_ONE) return "1"; + if (u === UNIT_ANY) return "_"; + + var unitStrs = []; + for (var unitName in u) { + if (!u.hasOwnProperty(unitName) || unitName === COUNT_FIELD) continue + + var power = u[unitName]; + if (_integerIsOne(power)) { + unitStrs = unitStrs.concat(unitName) + } else if (!_integerIsZero(power)) { + unitStrs = unitStrs.concat(unitName + " ^ " + power.toString()) + } + } + return unitStrs.sort().join(" * "); + }; + + var getUnit = function(n) { + if (n instanceof Unitnum) { + return n.u; + } else { + return UNIT_ONE; + } + }; + + var addUnit = function(n, u) { + return _withUnit(n, u, false); + }; + + var _unitExponentOf = function(u, key) { + if (u.hasOwnProperty(key)) { + return u[key]; + } else { + return 0; + } + }; + + // represent the unit as a list of objects, used by CPO + var unitToList = function(u) { + if (u === UNIT_ONE || u === UNIT_ANY) return []; + + var items = []; + for (var unitName in u) { + if (!u.hasOwnProperty(unitName) || unitName === COUNT_FIELD) continue + items.push({ name: unitName, pow: u[unitName] }); + } + + var comparator = function(a, b) { + if (a.name > b.name) { return 1 } + else if (a.name < b.name) { return -1 } + else { return 0 } + } + return items.sort(comparator); + } + + var _unitMap = function(u, f) { + if (u === UNIT_ONE || u === UNIT_ANY) return u; + + var newUnit = {}; + newUnit[COUNT_FIELD] = u[COUNT_FIELD] + for (var unitName in u) { + if (!u.hasOwnProperty(unitName) || unitName === COUNT_FIELD) continue + newUnit[unitName] = f(u[unitName]); + } + return newUnit; + } + + var _unitFilter = function(u, f) { + if (u === UNIT_ONE || u === UNIT_ANY) return u; + + var newUnit = {}; + newUnit[COUNT_FIELD] = 0; + for (var unitName in u) { + if (!u.hasOwnProperty(unitName) || !f(u[unitName]) || unitName === COUNT_FIELD) continue + newUnit[unitName] = u[unitName]; + newUnit[COUNT_FIELD] += 1; + } + + if (newUnit[COUNT_FIELD] === 0) return UNIT_ONE; + return newUnit; + } + + var checkUnit = function(u1, u2) { + if (u1 === UNIT_ANY || u2 === UNIT_ANY) return true; + if (u1 === UNIT_ONE || u2 === UNIT_ONE) return u1 === u2; + + for (var unitName in u1) { + if (!u1.hasOwnProperty(unitName)) continue + + if (!_integerEquals(_unitExponentOf(u1, unitName), _unitExponentOf(u2, unitName))) { + return false; + } + } + return true; + }; + + var _unitMerge = function(u1, u2) { + if (u1 === UNIT_ONE) return u2; + if (u2 === UNIT_ONE) return u1; + + var newUnit = {}; + for (var unitName in Object.assign({}, u1, u2)) { + if (!u1.hasOwnProperty(unitName) && !u2.hasOwnProperty(unitName)) continue + if (unitName === COUNT_FIELD) continue; + + var newExp = _integerAdd(_unitExponentOf(u1, unitName), _unitExponentOf(u2, unitName)); + if (!_integerIsZero(newExp)) { + newUnit[unitName] = newExp; + } + } + + var numKeys = Object.keys(newUnit).length + if (numKeys !== 0) { + newUnit[COUNT_FIELD] = Object.keys(newUnit).length + return newUnit; + } else { + return UNIT_ONE; + } + } + + var _unitInvert = function(u) { + return _unitMap(u, function(n) { return _integerMultiply(-1, n) }) + } + + var unitNumerator = function(u) { + return _unitFilter(u, function(pow) { return _integerGreaterThan(pow, 0) }); + } + + var unitDenominator = function(u) { + return unitNumerator(_unitInvert(u)); + } + + var ensureSameUnits = function(n1, n2, errbacks, opName) { + var u1 = getUnit(n1); + var u2 = getUnit(n2); + + if (!checkUnit(u1, u2)) { + errbacks.throwIncompatibleUnits(opName, unitToString(u1), unitToString(u2)); + } + } + + var _throwUnitsUnsupported = function(u, errbacks, opName) { + errbacks.throwGeneralError("The " + opName + " operation does not support units" + + " but was given an argument with the unit " + unitToString(u)) + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////// + // Integer operations // Integers are either represented as fixnums or as BigIntegers. @@ -1055,12 +1269,14 @@ define("pyret-base/js/js-numbers", function() { var makeIntegerBinop = function(onFixnums, onBignums, options) { options = options || {}; return (function(m, n) { - if (m instanceof Rational) { - m = numerator(m); + if (m instanceof Rational || m instanceof Unitnum) { + // TODO(benmusch): Is this okay? Can we always use fixnums? + // TODO(benmusch): Consider lifting both to unitnums + m = m.toFixnum(); } - if (n instanceof Rational) { - n = numerator(n); + if (n instanceof Rational || n instanceof Unitnum) { + n = n.toFixnum(); } if (typeof(m) === 'number' && typeof(n) === 'number') { @@ -1087,8 +1303,8 @@ define("pyret-base/js/js-numbers", function() { var makeIntegerUnOp = function(onFixnums, onBignums, options, errbacks) { options = options || {}; return (function(m) { - if (m instanceof Rational) { - m = numerator(m); + if (m instanceof Rational || m instanceof Unitnum) { + m = m.toFixnum(); } if (typeof(m) === 'number') { @@ -1333,11 +1549,26 @@ define("pyret-base/js/js-numbers", function() { // isRational: -> boolean // Produce true if the number is rational. + // isRoughnum: -> boolean + // Produce true if the number is roughnum. + // isExact === isRational // isReal: -> boolean // Produce true if the number is real. + // isPositive: -> boolean + // Produce true if the number is positive. + + // isNonNegative: -> boolean + // Produce true if the number is positive. + + // isNegative: -> boolean + // Produce true if the number is positive. + + // isNonPositive: -> boolean + // Produce true if the number is positive. + // toRational: -> pyretnum // Produce an exact number. @@ -1400,6 +1631,9 @@ define("pyret-base/js/js-numbers", function() { // atan: -> pyretnum // Produce the arc tangent. + // tan: -> pyretnum + // Produce the arc tangent. + // cos: -> pyretnum // Produce the cosine. @@ -1421,11 +1655,210 @@ define("pyret-base/js/js-numbers", function() { // round: -> pyretnum // Round to the nearest integer. + // roundEven: -> pyretnum + // Round to the nearest integer, returning an exactnum if the number is + // between integers + // equals: pyretnum -> boolean // Produce true if the given number of the same type is equal. ////////////////////////////////////////////////////////////////////// + // Unitnums + var Unitnum = function(n, u) { + this.n = n; + this.u = u; + }; + + Unitnum.prototype.toString = function() { + var unitStr = "%<" + unitToString(this.u) + ">" + return this.n.toString() + unitStr; + }; + + Unitnum.prototype.isFinite = function() { + return typeof(this.n) === "number" || this.n.isFinite(); + }; + + Unitnum.prototype.isInteger = function() { + return isInteger(this.n); + }; + + Unitnum.prototype.isRational = function() { + return isRational(this.n); + }; + + Unitnum.prototype.isRoughnum = function() { + return isRoughnum(this.n); + }; + + Unitnum.prototype.isExact = Unitnum.prototype.isRational; + + Unitnum.prototype.isPositive = function() { + return isPositive(this.n); + }; + + Unitnum.prototype.isNonNegative = function() { + return isNonNegative(this.n); + }; + + Unitnum.prototype.isNegative = function() { + return isNegative(this.n); + }; + + Unitnum.prototype.isNonPositive = function() { + return isNonPositive(this.n); + }; + + Unitnum.prototype.toRational = function() { + return _withUnit(toRational(this.n), this.u, false); + } + + Unitnum.prototype.toExact = Unitnum.prototype.toRational; + + Unitnum.prototype.toRoughnum = function() { + return _withUnit(toRoughnum(this.n), this.u, false); + }; + + Unitnum.prototype.toFixnum = function() { + return toFixnum(this.n); + }; + + Unitnum.prototype.greaterThan = function(n, errbacks) { + ensureSameUnits(this, n, errbacks, "> operation"); + return greaterThan(_withoutUnit(this.n), _withoutUnit(n)); + }; + + Unitnum.prototype.greaterThanOrEqual = function(n, errbacks) { + ensureSameUnits(this, n, errbacks, ">= operation"); + return greaterThanOrEqual(_withoutUnit(this.n), _withoutUnit(n)); + }; + + Unitnum.prototype.lessThan = function(n, errbacks) { + ensureSameUnits(this, n, errbacks, "< operation"); + return lessThan(_withoutUnit(this.n), _withoutUnit(n)); + }; + + Unitnum.prototype.lessThanOrEqual = function(n, errbacks) { + ensureSameUnits(this, n, errbacks, "<= operation"); + return lessThanOrEqual(_withoutUnit(this.n), _withoutUnit(n)); + }; + + Unitnum.prototype.add = function(n, errbacks) { + ensureSameUnits(this, n, errbacks, "+ operation"); + return _withUnit(add(_withoutUnit(this), _withoutUnit(n), errbacks), this.u, false); + }; + + Unitnum.prototype.subtract = function(n, errbacks) { + ensureSameUnits(this, n, errbacks, "- operation"); + return _withUnit(subtract(_withoutUnit(this), _withoutUnit(n), errbacks), this.u, false); + }; + + Unitnum.prototype.multiply = function(n, errbacks) { + var newUnit = _unitMerge(this.u, n.u); + return _withUnit(multiply(_withoutUnit(this), _withoutUnit(n), errbacks), newUnit, false); + }; + + Unitnum.prototype.divide = function(n, errbacks) { + var newUnit = _unitMerge(this.u, _unitInvert(n.u)); + return _withUnit(divide(_withoutUnit(this), _withoutUnit(n), errbacks), newUnit, false); + }; + + Unitnum.prototype.numerator = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "numerator"); + }; + + Unitnum.prototype.denominator = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "denominator"); + }; + + Unitnum.prototype.integerSqrt = function(errbacks) { + var that = this; + var newUnit = _unitMap(this.u, function(pow) { + if (!_integerIsZero(_integerModulo(pow, 2))) { + errbacks.throwInvalidUnitState("num-sqrt", that, "not all units had an even exponent"); + } else { + return halve(pow, errbacks); + } + }); + return _withUnit(integerSqrt(this.n), newUnit, false); + }; + + Unitnum.prototype.sqrt = function(errbacks) { + var that = this; + var newUnit = _unitMap(this.u, function(pow) { + if (!_integerIsZero(_integerModulo(pow, 2))) { + errbacks.throwInvalidUnitState("num-sqrt", that, "not all units had an even exponent"); + } else { + return halve(pow, errbacks); + } + }); + return _withUnit(sqrt(this.n), newUnit, false); + }; + + Unitnum.prototype.abs = function() { + return _withUnit(abs(this.n), this.u, false); + }; + + Unitnum.prototype.floor = function() { + return _withUnit(floor(this.n), this.u, false); + }; + + Unitnum.prototype.ceiling = function() { + return _withUnit(ceiling(this.n), this.u, false); + }; + + Unitnum.prototype.log = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "log"); + }; + + Unitnum.prototype.tan = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "tan"); + }; + + Unitnum.prototype.atan = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "atan"); + }; + + Unitnum.prototype.cos = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "cos"); + }; + + Unitnum.prototype.sin = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "sin"); + }; + + Unitnum.prototype.expt = function(n, errbacks) { + if (!isInteger(n)) { + errbacks.throwInvalidUnitState("num-expt", n, "a number with a unit cannot be raised to a non-integer power"); + } + var newUnit = _unitMap(this.u, function(pow) { return n * pow }); + return _withUnit(expt(this.n, n), newUnit, false); + }; + + Unitnum.prototype.exp = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "exp", false); + }; + + Unitnum.prototype.acos = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "acos", false); + }; + + Unitnum.prototype.asin = function(errbacks) { + _throwUnitsUnsupported(this.u, errbacks, "asin", false); + }; + + Unitnum.prototype.round = function() { + return _withUnit(round(this.n), this.u, false); + }; + + Unitnum.prototype.roundEven = function() { + return _withUnit(roundEven(this.n), this.u, false); + }; + + Unitnum.prototype.equals = function(other) { + return checkUnit(this.u, getUnit(other)) && equals(this.n, other.n); + }; + // Rationals var Rational = function(n, d) { @@ -3937,6 +4370,10 @@ define("pyret-base/js/js-numbers", function() { if (!isInteger(digits)) { errbacks.throwDomainError('num-to-string-digits: digits should be an integer'); } + + if (n instanceof Unitnum) { + return toStringDigits(n.n, digits, errbacks) + "%<" + unitToString(n.u) + ">"; + } var tenDigits = expt(10, digits, errbacks); var d = toFixnum(digits); n = divide(round(multiply(n, tenDigits, errbacks), errbacks), tenDigits, errbacks); @@ -3951,7 +4388,7 @@ define("pyret-base/js/js-numbers", function() { return ans; } // n is not an integer implies that d >= 1 - var decimal = toRepeatingDecimal(n.numerator(), n.denominator(), undefined, errbacks); + var decimal = toRepeatingDecimal(numerator(n), denominator(n), undefined, errbacks); var ans = decimal[1].toString(); while (ans.length < d) { ans += decimal[2]; @@ -3968,9 +4405,20 @@ define("pyret-base/js/js-numbers", function() { Numbers['makeBignum'] = makeBignum; Numbers['makeRational'] = Rational.makeInstance; Numbers['makeRoughnum'] = Roughnum.makeInstance; + Numbers['addUnit'] = addUnit; + Numbers['getUnit'] = getUnit; + Numbers['checkUnit'] = checkUnit; + Numbers['ensureSameUnits'] = ensureSameUnits; + Numbers['unitToString'] = unitToString; + Numbers['unitNumerator'] = unitNumerator; + Numbers['unitDenominator'] = unitDenominator; + Numbers['unitToList'] = unitToList; + Numbers['UNIT_ONE'] = UNIT_ONE; + Numbers['UNIT_ANY'] = UNIT_ANY; Numbers['isPyretNumber'] = isPyretNumber; Numbers['isRational'] = isRational; + Numbers['isUnitnum'] = isUnitnum; Numbers['isReal'] = isReal; Numbers['isExact'] = isExact; Numbers['isInteger'] = isInteger; diff --git a/src/js/base/pyret-grammar.bnf b/src/js/base/pyret-grammar.bnf index a94248e28c..513e34a3e4 100644 --- a/src/js/base/pyret-grammar.bnf +++ b/src/js/base/pyret-grammar.bnf @@ -130,9 +130,17 @@ id-expr: NAME prim-expr: num-expr | frac-expr | rfrac-expr | bool-expr | string-expr -num-expr: NUMBER -frac-expr: RATIONAL -rfrac-expr: ROUGHRATIONAL +dim-expr: (LANGLE|LT) unit-expr (RANGLE|GT) +unit-atom: (PARENNOSPACE|PARENSPACE) unit-expr RPAREN + | NAME + | unit-atom CARET NUMBER + | NUMBER +unit-expr: unit-atom ((STAR|SLASH) unit-atom)* + + +num-expr: NUMBER [PERCENT dim-expr] +frac-expr: RATIONAL [PERCENT dim-expr] +rfrac-expr: ROUGHRATIONAL [PERCENT dim-expr] bool-expr: TRUE | FALSE string-expr: STRING @@ -233,7 +241,7 @@ load-table-spec: SOURCECOLON expr user-block-expr: BLOCK block END -ann: name-ann | record-ann | arrow-ann | app-ann | pred-ann | dot-ann | tuple-ann +ann: name-ann | record-ann | arrow-ann | app-ann | pred-ann | dot-ann | tuple-ann | unit-ann name-ann: NAME comma-ann-field: ann-field (COMMA ann-field)* @@ -251,6 +259,9 @@ app-ann: (name-ann|dot-ann) LANGLE comma-anns (RANGLE|GT) comma-anns: ann (COMMA ann)* +# TODO: Can we enforce ordering here? +unit-ann: name-ann PERCENT dim-expr + pred-ann: ann PERCENT (PARENSPACE|PARENNOSPACE) id-expr RPAREN dot-ann : NAME DOT NAME diff --git a/src/js/base/runtime.js b/src/js/base/runtime.js index 2c3e17fa70..c06d3580a4 100644 --- a/src/js/base/runtime.js +++ b/src/js/base/runtime.js @@ -617,7 +617,8 @@ function (Namespace, jsnums, codePoint, util, exnStackParser, loader, seedrandom /**Makes a PNumber using the given string @param {string} s - @return {!PNumber} with value n + @param {unit} u + @return {!PNumber} with value n and unit u */ function makeNumberFromString(s) { var result = jsnums.fromString(s, NumberErrbacks); @@ -1986,18 +1987,29 @@ function (Namespace, jsnums, codePoint, util, exnStackParser, loader, seedrandom } curLeft = current.left; curRight = current.right; + var isIdentical = thisRuntime.ffi.isEqual(identical3(curLeft, curRight)) - if (thisRuntime.ffi.isEqual(identical3(curLeft, curRight))) { + if (!fromWithin && isIdentical) { + continue; + } else if (isIdentical && !(isNumber(curLeft) || isNumber(curRight)) && !jsnums.isUnitnum(tol)) { + // when this equality check is from a within call and the tolerance + // has a unit, we can't just check for equality because we need to + // check for unit-mismatches between the tolerance and compared values + // see commits 7c66302 and 7ddfc67 continue; } else if (isNumber(curLeft) && isNumber(curRight)) { - if (tol) { + if (!jsnums.checkUnit(jsnums.getUnit(curLeft), jsnums.getUnit(curRight))) { + toCompare.curAns = thisRuntime.ffi.notEqual.app(current.path + ".units", curLeft, curRight); + } else if (tol) { if (rel) { - if (jsnums.roughlyEqualsRel(curLeft, curRight, tol, NumberErrbacks)) { + if (jsnums.roughlyEqualsRel(curLeft, curRight, tol, "equality", NumberErrbacks)) { continue; } else { toCompare.curAns = thisRuntime.ffi.notEqual.app(current.path, curLeft, curRight); } - } else if (jsnums.roughlyEquals(curLeft, curRight, tol, NumberErrbacks)) { + } else if (!jsnums.checkUnit(jsnums.getUnit(curLeft), jsnums.getUnit(tol))) { + toCompare.curAns = thisRuntime.ffi.notEqual.app(current.path + ".tolerance-units", curLeft, curRight); + } else if (jsnums.roughlyEquals(curLeft, curRight, tol, "equality", NumberErrbacks)) { continue; } else { toCompare.curAns = thisRuntime.ffi.notEqual.app(current.path, curLeft, curRight); @@ -2211,7 +2223,7 @@ function (Namespace, jsnums, codePoint, util, exnStackParser, loader, seedrandom function equalWithinAbsNow3(tol) { if (arguments.length !== 1) { var $a=new Array(arguments.length); for (var $i=0;$i"] + A.s-instantiate(d, A.s-num(d, 0, A.u-one(d)), [list: A.a-any(d)]).tosource().pretty(80) is [list: "0"] end diff --git a/tests/pyret/tests/test-contracts.arr b/tests/pyret/tests/test-contracts.arr index 9e5d9b8f95..da8cbe5f9c 100644 --- a/tests/pyret/tests/test-contracts.arr +++ b/tests/pyret/tests/test-contracts.arr @@ -457,3 +457,102 @@ check "standalone contract statements": ```) is%(output) success end +check "Should notice unit mis-matches": + run-str( + ``` + fun id(n :: Number): n end + id(1%) + ```) is%(output) contract-error + run-str( + ``` + fun id(n :: Number%): n end + id(1%) + ```) is%(output) contract-error + run-str( + ``` + fun id(n :: Number%%(num-is-integer)): n end + id(1%) + ```) is%(output) contract-error + run-str( + ``` + type N = Number%(num-is-integer) + fun id(n :: N%): n end + id(1%) + ```) is%(output) contract-error + run-str( + ``` + type N = Number%%(num-is-integer) + fun id(n :: N): n end + id(1%) + ```) is%(output) contract-error + run-str( + ``` + type N = Number%%(num-is-integer) + fun id(n :: N%): n end + id(1%) + ```) is%(output) contract-error + run-str( + ``` + fun id(n :: Any%): n end + id(1%) + ```) is%(output) contract-error + run-str( + ``` + fun id(n :: {Number%; Number%}): n end + id({1%; 1%}) + ```) is%(output) contract-error + run-str( + ``` + fun id(n :: { a :: Number%, b :: Number%}): n end + id({ a: 1%, b: 1%}) + ```) is%(output) contract-error + run-str( + ``` + type MyAny = Any%<1> + fun id(n :: MyAny%): n end + id(1%) + ```) is%(output) contract-error + + run-str( + ``` + fun id(n :: Number%%(num-is-integer)): n end + id(1%) + ```) is%(output) success + run-str( + ``` + fun id(n :: Number%<_>%(num-is-integer)): n end + id(1%) + ```) is%(output) success + run-str( + ``` + fun id(n :: Number%<_>%(num-is-integer)): n end + id(1) + ```) is%(output) success + run-str( + ``` + fun id(n :: Any): n end + id(1%) + ```) is%(output) success + run-str( + ``` + fun id(n :: Any%): n end + id(1%) + ```) is%(output) success + run-str( + ``` + fun id(n :: {Number%; Number%}): n end + id({1%; 1%}) + ```) is%(output) success + run-str( + ``` + type N = Number%%(num-is-integer) + fun id(n :: { a :: Number%, b :: Number%}): n end + id({ a: 1%, b: 1%}) + ```) is%(output) success + run-str( + ``` + type MyAny = Any + fun id(n :: MyAny): n end + id(1%) + ```) is%(output) success +end diff --git a/tests/pyret/tests/test-parse.arr b/tests/pyret/tests/test-parse.arr index c5e9ff6fb6..3eadcf9abb 100644 --- a/tests/pyret/tests/test-parse.arr +++ b/tests/pyret/tests/test-parse.arr @@ -468,3 +468,46 @@ check "should parse reactors": does-parse("reactor: end") is false does-parse("reactor end") is false end + +check "should parse unit-annotated numbers": + does-parse("2%") is true + does-parse("2%<(m)>") is true + does-parse("2%") is true + does-parse("2%") is true + does-parse("2%") is true + does-parse("2%") is true + does-parse("2%") is true # should be wf-error + does-parse("2%<(m / n) ^ -5 * (o ^ 10)>") is true + does-parse("2%<1>") is true + does-parse("2%<1 * m>") is true + + does-parse("2%<>") is false + does-parse("2%<()>") is false + does-parse("2%") is false + does-parse("2%") is false + does-parse("2%") is false + does-parse("2%") is false + does-parse("2%") is false + does-parse("2%") is false + does-parse("2%") is false + does-parse("2%") is false + does-parse("2%<2>") raises "" + does-parse("2%<-2>") raises "" +end + +check "should parse unit-anns numbers": + does-parse("n :: Number%% = 0") is false + does-parse("n :: Number%%(is-even)% = 0") is false + does-parse("n :: Number%(is-two)% = 0") is false + does-parse("n :: Number%<>%(is-even) = 0") is false + does-parse("n :: Number%<> = 0") is false + + # should be caught by wf: + does-parse("n :: Number% = 0") is true + + does-parse("n :: Number%%(is-even) = 0") is true + does-parse("n :: Number% = 0") is true + does-parse("n :: Number%%(is-even) = 0") is true + does-parse("n :: Number%%(is-two)%(is-even) = 0") is true + does-parse("n :: String% = 'foo'") is true +end diff --git a/tests/pyret/tests/test-units.arr b/tests/pyret/tests/test-units.arr new file mode 100644 index 0000000000..a93a287f0c --- /dev/null +++ b/tests/pyret/tests/test-units.arr @@ -0,0 +1,216 @@ +import error as E +import contracts as C + +fun is-unit-contract-fail(result): + C.is-fail(result) and C.is-failure-at-arg(result.reason) and C.is-unit-fail(result.reason.reason) +end + + +check "Unit equality": + 2 == 2% is false + 2% == 2% is false + + 2% is 2% + 2%<1 / m> is 2% + 2%<1> is 2 + 2%<1 * m> is 2% + 2% is 2% + 2 is 2% + 2% is 2% + 2%<(((s * m) ^ 2) / t) ^ 2> is 2% + + 1/1% is 1% + ~1/2% is-roughly ~1/2% + + # tests for adding/removing units + (10 * 1%) + 1% is 11% + (10% / 1%) + 1 is 11 + + 0% == 0% is false + + 2% =~ 2% is true + 2% =~ 2% is false + 2% <=> 2% is true + 2% <=> 2% is false +end + +check "Arithmetic binops": + 2% + 2% is 4% + (2% + 2%) raises-satisfies E.is-incompatible-units + (2% + 0%) raises-satisfies E.is-incompatible-units + + 2% - 2% is 0% + (2% - 2%) raises-satisfies E.is-incompatible-units + (2% - 0%) raises-satisfies E.is-incompatible-units + + 2% * 2% is 4% + 2% * 2% is 4 + 2% * 2% is 4% + 2 * 2% is 4% + + 2% / 2% is 1 + 2% / 2% is 1% + 2% / 2% is 1% + 2% / 0% raises "/: division by zero, 2% 0%" +end + +check "Logical binops": + 2% > 1% is true + 2% > 2% is false + 2% > 2% raises-satisfies E.is-incompatible-units + + 2% >= 2% is true + 2% >= 3% is false + 2% >= 2% raises-satisfies E.is-incompatible-units + + 1% < 2% is true + 2% < 2% is false + 2% < 2% raises-satisfies E.is-incompatible-units + + 2% <= 2% is true + 2% <= 1% is false + 2% <= 2% raises-satisfies E.is-incompatible-units +end + +check "Other supported operations": + num-equal(2%, 2%) is true + num-equal(4/2%, 2%) is true + num-equal(4/2, 2%) is false + + num-to-roughnum(2%) is-roughly ~2.0% + + num-is-integer(2%) is true + num-is-integer(2.5%) is false + + num-is-rational(2%) is true + num-is-rational(2.5%) is true + num-is-rational(1/2%) is true + num-is-rational(~2%) is false + + num-is-roughnum(2%) is false + num-is-roughnum(1/2%) is false + num-is-roughnum(1.609%) is false + num-is-roughnum(~2%) is true + + num-is-positive(2%) is true + num-is-positive(0%) is false + num-is-positive(-2%) is false + num-is-non-positive(2%) is false + num-is-non-positive(0%) is true + num-is-non-positive(-2%) is true + + num-is-negative(2%) is false + num-is-negative(0%) is false + num-is-negative(-2%) is true + num-is-non-negative(2%) is true + num-is-non-negative(0%) is true + num-is-non-negative(-2%) is false + + num-to-string(2.5%) is "5/2%" + num-to-string(2%) is "2%" + num-to-string(2/3%) is "2/3%" + num-to-string(~2.718%) is "~2.718%" + num-to-string(~6.022e23%) is "~6.022e+23%" + + num-to-string-digits(2/3%, 3) is "0.667%" + num-to-string-digits(-2/3%, 3) is "-0.667%" + + num-to-string-digits(5%, 2) is "5.00%" + num-to-string-digits(5%, 0) is "5%" + num-to-string-digits(555%, -2) is "600%" + + num-max(2%, 1%) is 2% + num-max(2%, 2%) raises-satisfies E.is-incompatible-units + num-min(2%, 1%) is 1% + num-min(2%, 2%) raises-satisfies E.is-incompatible-units + + num-abs(-2%) is 2% + num-floor(3/2%) is 1% + num-ceiling(3/2%) is 2% + num-round(3/2%) is 2% + num-round-even(3.5%) is 4% + num-truncate(3.5%) is 3% + + num-sqr(3%) is 9% + num-expt(2%, 3) is 8% + num-expt(2%, -3) is 1/8%<(s / m) ^ 3> + num-sqrt(4%) is 2% + num-sqrt(4%) is 2%<1 / m> +end + +check "Unsupported operations": + num-sqrt(2%) raises-satisfies E.is-invalid-unit-state + num-expt(2%, 0.5) raises-satisfies E.is-invalid-unit-state + + num-log(2%) raises-satisfies is-unit-contract-fail + num-atan(2%) raises-satisfies is-unit-contract-fail + num-atan2(2%, 2) raises-satisfies is-unit-contract-fail + num-atan2(2, 2%) raises-satisfies is-unit-contract-fail + num-asin(0%) raises-satisfies is-unit-contract-fail + num-acos(0%) raises-satisfies is-unit-contract-fail + num-sin(2%) raises-satisfies is-unit-contract-fail + num-tan(2%) raises-satisfies is-unit-contract-fail + num-cos(2%) raises-satisfies is-unit-contract-fail + num-exp(2%) raises-satisfies is-unit-contract-fail + num-expt(2%, 3%) raises-satisfies is-unit-contract-fail + num-modulo(2%, 2%) raises-satisfies is-unit-contract-fail + num-to-string-digits(2/3%, 3%) raises-satisfies is-unit-contract-fail +end + +check "Units with bigint powers": + 1% == 1% is false + 1% == 1% is true + + num-sqr(1%) == 1% is true + num-sqrt(1%) == 1% is true + num-sqr(1%) == 1% is false + num-sqrt(1%) == 1% is false + + num-sqrt(num-sqrt(num-sqrt(num-sqr(num-sqr(num-sqr(1%)))))) is 1% +end + +check "Within builtins": + within(2%) raises-satisfies is-unit-contract-fail + + within(0.5)(3%, 5/2%) is true + within(0.5)(3%, ~2.999999%) is true + within(0.5)(3%, 2%) is true + within(0.5)(3%, 1%) is false + within(0.5)([list: 1%, 1%], [list: 1%, 4%]) is false + within(0.5)([list: 1%, 2%], [list: 1%, 2.5%]) is true + + within(0.5)(3%, 2) is false + within(0.5)(3%, 2%

) is false + + within-abs(1%)(2%, 5/2%) is true + within-abs(1%)(2%, ~2.99999%) is true + within-abs(1%)(2%, 3%) is true + within-abs(1%)(2%, 4%) is false + within-abs(1%)([list: 1%, 2%], [list: 1%, 4%]) is false + within-abs(1%)([list: 1%, 3.5%], [list: 1%, 4%]) is true + + within-abs(1)(2, 3%) is false + within-abs(1%)(2, 3%) is false + within-abs(1)(2%, 3%

) is false + within-abs(1%

)(2%, 3%) is false + + num-within-rel(2%) raises-satisfies is-unit-contract-fail + + num-within-rel(0.5)(3%, 5/2%) is true + num-within-rel(0.5)(3%, ~2.999999%) is true + num-within-rel(0.5)(3%, 2%) is true + num-within-rel(0.5)(3%, 1%) is false + + num-within-rel(0.5)(3%, 2) raises-satisfies E.is-incompatible-units + num-within-rel(0.5)(3%, 2%

) raises-satisfies E.is-incompatible-units + + num-within-abs(1%)(2%, 5/2%) is true + num-within-abs(1%)(2%, ~2.99999%) is true + num-within-abs(1%)(2%, 3%) is true + num-within-abs(1%)(2%, 4%) is false + + num-within-abs(1)(2, 3%) raises-satisfies E.is-incompatible-units + num-within-abs(1%)(2, 3%) raises-satisfies E.is-incompatible-units + num-within-abs(1)(2%, 3%

) raises-satisfies E.is-incompatible-units + num-within-abs(1%

)(2%, 3%) raises-satisfies E.is-incompatible-units +end diff --git a/tests/pyret/tests/test-well-formed.arr b/tests/pyret/tests/test-well-formed.arr index b23c3d7afd..a6b307ef4a 100644 --- a/tests/pyret/tests/test-well-formed.arr +++ b/tests/pyret/tests/test-well-formed.arr @@ -261,6 +261,34 @@ check "underscores": run-str("{a: 1}.{_: 2}") is%(output) compile-error(CS.is-underscore-as) run-str("{a: 1}._") is%(output) compile-error(CS.is-underscore-as) + + run-str("2%<_>") is%(output) compile-error(CS.is-underscore-as-unit) + run-str("n :: Number%<_ * m> = 1") is%(output) compile-error(CS.is-underscore-as-unit) + run-str("n :: Number%<_ / m> = 1") is%(output) compile-error(CS.is-underscore-as-unit) + run-str("n :: Number%<(_)> = 1") is%(output) compile-error(CS.is-underscore-as-unit) + run-str("n :: Number%<_ ^ 2> = 1") is%(output) compile-error(CS.is-underscore-as-unit) +end + +check "unit annotations": + run-str("2%") is%(output) compile-error(CS.is-mixed-unit-ops) + run-str("2%") is%(output) compile-error(CS.is-mixed-unit-ops) + run-str("1/2%") is%(output) compile-error(CS.is-mixed-unit-ops) + run-str("1/2%") is%(output) compile-error(CS.is-mixed-unit-ops) + run-str("~1/2%") is%(output) compile-error(CS.is-mixed-unit-ops) + run-str("~1/2%") is%(output) compile-error(CS.is-mixed-unit-ops) + run-str("var n :: Number% = 0") is%(output) compile-error(CS.is-mixed-unit-ops) + run-str("var n :: Number% = 0") is%(output) compile-error(CS.is-mixed-unit-ops) + + run-str("2%") is%(output) compile-error(CS.is-invalid-unit-power) + run-str("2%") is%(output) compile-error(CS.is-invalid-unit-power) + run-str("1/2%") is%(output) compile-error(CS.is-invalid-unit-power) + run-str("1/2%") is%(output) compile-error(CS.is-invalid-unit-power) + run-str("~1/2%") is%(output) compile-error(CS.is-invalid-unit-power) + run-str("~1/2%") is%(output) compile-error(CS.is-invalid-unit-power) + run-str("var n :: Number% = 0") is%(output) compile-error(CS.is-invalid-unit-power) + run-str("var n :: Number% = 0") is%(output) compile-error(CS.is-invalid-unit-power) + + run-str("2%<1 ^ 2>") is%(output) compile-error(CS.is-one-as-power-base) end #| diff --git a/tests/pyret/tests/test-within.arr b/tests/pyret/tests/test-within.arr index 1bedd762be..fcf504c868 100644 --- a/tests/pyret/tests/test-within.arr +++ b/tests/pyret/tests/test-within.arr @@ -181,3 +181,25 @@ check "all within variants are defined": within-abs-now3 does-not-raise within3 does-not-raise end + +check "comparing strings": + within(0)("dog", "dog") is true + within(0)("dog", "Dog") is false + within(0.5)("dog", "Dog") is true + within(0.25)("dog", "Dog") is false + + within-abs(0)("dog", "dog") is true + within-abs(0)("dog", "Dog") is false + within-abs(1)("dog", "Dog") is true + within-abs(1)("dog", "DOg") is false + + within(0)("dog", "dog") is true + within(0)("dog", "Dog") is false + within(0.5)("dog", "Dog") is true + within(0.25)("dog", "Dog") is false + + within-abs(0)("dog", "dog") is true + within-abs(0)("dog", "Dog") is false + within-abs(1)("dog", "Dog") is true + within-abs(1)("dog", "DOg") is false +end