Skip to content

Commit 742ba2e

Browse files
authored
add macro to parse literals (#12)
* add macro to parse literals * update changelog * bump version
1 parent b2b3369 commit 742ba2e

File tree

7 files changed

+95
-11
lines changed

7 files changed

+95
-11
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "DedekindCutArithmetic"
22
uuid = "a9cf20ff-59c8-5762-8ce8-520b700dfeff"
33
authors = ["Luca Ferranti"]
4-
version = "0.1.0"
4+
version = "0.1.1"
55

66
[deps]
77
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"

docs/src/changelog.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ EditURL = "https://github.com/lucaferranti/DedekindCutArithmetic.jl/blob/main/CH
1010

1111
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1212

13-
## Unreleased
13+
## [v0.1.1](https://github.com/lucaferranti/DedekindCutArithmetic.jl/releases/tag/v0.1.1) -- 2025-04-13
1414

1515
### Added
1616

1717
- Support `Base.one` and `Base.zero` for cuts ([#5](https://github.com/lucaferranti/DedekindCutArithmetic.jl/issues/5))
1818
- Support `^` operation with non-negative integer exponent ([#4](https://github.com/lucaferranti/DedekindCutArithmetic.jl/issues/4))
1919
- Support `inv` and division ([#9](https://github.com/lucaferranti/DedekindCutArithmetic.jl/issues/9))
20+
- Add `@exact_str` string macro to correctly parse float literals ([#12](https://github.com/lucaferranti/DedekindCutArithmetic.jl/issues/12))
21+
- Add non-unicode `@forall` and `@exists` aliases ([#12](https://github.com/lucaferranti/DedekindCutArithmetic.jl/issues/12))
2022

2123
## [v0.1.0](https://github.com/lucaferranti/DedekindCutArithmetic.jl/releases/tag/v0.1.0) -- 2024-10-25
2224

docs/src/tutorial.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,42 @@ we can now use that to compute the maximum of ``f(x) = x(1 - x)``, which should
192192
```@repl tutorial1
193193
my_max(x -> x * (1 - x))
194194
```
195+
196+
## Working with float literals
197+
198+
By default, real numbers on compuers are approximately represented via floating point numbers.
199+
Floats cannot represent all reals exactly and when typing decimals, e.g. `0.1` one does not get the
200+
exact value, but an approximation
201+
202+
```@repl tutorial1
203+
a = 0.1
204+
205+
a == 1//10
206+
207+
big(a)
208+
```
209+
210+
Unfortunately, the parsing of literals in Julia happens already before building the abstract syntax tree,
211+
hence when processing expressions with macros, the rounding sin has already happened.
212+
213+
For example, the following would fail to terminate
214+
215+
```julia
216+
@∀ x [0, 1]: x + 0.1000000000000000000001 > x + 0.1
217+
```
218+
219+
because when parsing the literals both produce the same floating point number.
220+
221+
```@repl tutorial1
222+
0.1000000000000000000001 == 0.1
223+
```
224+
225+
When working with rational numbers in decimal form, use the [`@exact_str`](@ref) string macro, which ensures the literal is parsed to the correct rational number.
226+
227+
```@repl tutorial1
228+
exact"0.1"
229+
```
230+
231+
```@repl tutorial1
232+
@∀ x ∈ [0, 1]: x + exact"0.1000000000000000000001" > x + exact"0.1"
233+
```

src/DedekindCutArithmetic.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ using ForwardDiff
1212
export DyadicReal, DyadicInterval, DedekindCut, RationalCauchyCut, BinaryCompositeCut, @cut,
1313
refine!, dual, overlaps,
1414
width, midpoint, radius, thirds, low, high, isforward, isbackward,
15-
exists, forall, @∀, @∃
15+
exists, forall, @∀, @∃, @forall, @exists,
16+
@exact_str
1617

1718
const _Real = Union{Integer, AbstractFloat, Rational}
1819

src/macros.jl

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ end
1313
"""
1414
Check whether ``∃ x ∈ dom : prop(x)``
1515
"""
16-
macro (ex)
16+
macro exists(ex)
1717
parse_quantifier_body(ex, :exists)
1818
end
19+
const var"@∃" = var"@exists"
1920

2021
"""
2122
Check whether ``∀ x ∈ dom : prop(x)``
2223
"""
23-
macro (ex)
24+
macro forall(ex)
2425
parse_quantifier_body(ex, :forall)
2526
end
27+
const var"@∀" = var"@forall"
2628

2729
"""
2830
Transformations on the expression before being processed by [`@cut`](@ref)
@@ -73,3 +75,41 @@ macro cut(ex)
7375
throw(ArgumentError("Invalid cut expression $ex"))
7476
end
7577
end
78+
79+
function parse_decimal(s::AbstractString)
80+
num_exp = split(s, ('e', 'E'))
81+
1 <= length(num_exp) <= 2 || throw(Meta.ParseError("invalid literal $s"))
82+
n = first(num_exp)
83+
int_dec = split(n, '.')
84+
1 <= length(int_dec) <= 2 || throw(Meta.ParseError("invalid literal $s"))
85+
num, logden = if length(int_dec) == 2
86+
int, dec = int_dec
87+
parse(BigInt, int * dec), -length(dec)
88+
else
89+
parse(BigInt, first(int_dec)), 0
90+
end
91+
if length(num_exp) == 2
92+
logden += parse(BigInt, last(num_exp))
93+
end
94+
if logden < 0
95+
num // 10^(-logden)
96+
else
97+
num * 10^logden
98+
end
99+
end
100+
101+
"""
102+
Parse a decimal literal into a Rational{BigInt}
103+
104+
```julia
105+
julia> exact"0.1"
106+
1//10
107+
```
108+
109+
This is needed because literals are already parsed before constructing the AST, hence
110+
when writing :(x - 0.1) one would get the floating point approximation of 1//10 instead of the
111+
exact rational.
112+
"""
113+
macro exact_str(s::AbstractString)
114+
parse_decimal(s)
115+
end

src/quantifiers.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
Check whether ``∃ x ∈ dom : f(x) < 0``
33
"""
44
function exists(dom::DyadicInterval, f::Function)::Bool
5-
high(f(midpoint(dom))) < 0 && return true
5+
f(midpoint(dom)) < 0 && return true
66

77
# try to avoid bisection by checking for monotonicity
88
d = ForwardDiff.derivative(f, dom)
9-
low(d) > 0 && low(f(low(dom))) >= 0 && return false
10-
high(d) < 0 && low(f(high(dom))) >= 0 && return false
9+
low(d) > 0 && f(low(dom)) >= 0 && return false
10+
high(d) < 0 && f(high(dom)) >= 0 && return false
1111

1212
high(f(dual(dom))) >= 0 && return false
1313

@@ -19,12 +19,12 @@ end
1919
Check whether ``∀ x ∈ dom : f(x) < 0``
2020
"""
2121
function forall(dom::DyadicInterval, f::Function)::Bool
22-
low(f(midpoint(dom))) >= 0 && return false
22+
f(midpoint(dom)) >= 0 && return false
2323

2424
# try to avoid bisection by checking for monotonicity
2525
d = ForwardDiff.derivative(f, dom)
26-
low(d) > 0 && high(f(high(dom))) < 0 && return true
27-
high(d) < 0 && high(f(low(dom))) < 0 && return true
26+
low(d) > 0 && f(high(dom)) < 0 && return true
27+
high(d) < 0 && f(low(dom)) < 0 && return true
2828

2929
high(f(dom)) < 0 && return true
3030

test/test-quantifiers.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ using Test
77
@test @∀ x [0, 1]:x < 2
88
@test @∀ x [0, 1]:x < 1.0001
99

10+
@test @∀ x [0, 1]:x > exact"-0.1e-12"
11+
1012
@test !(@∀ x [0, 1]:x > 0.0001)
1113
@test !(@∀ x [0, 1]:x < 0.9999)
1214
end

0 commit comments

Comments
 (0)