Crates.io | fractran |
lib.rs | fractran |
version | 0.1.0 |
source | src |
created_at | 2020-05-26 22:06:48.856428 |
updated_at | 2020-05-26 22:06:48.856428 |
description | A library to execute Fractran programs |
homepage | https://github.com/nicholas-miklaucic/fractran-lib-rs |
repository | https://github.com/nicholas-miklaucic/fractran-lib-rs |
max_upload_size | |
id | 246367 |
size | 33,219 |
This is a library that implements the FRACTRAN esoteric programming language,
invented by John Conway, in Rust. It supplies a basic way of running programs in
FRACTRAN and additionally implements a custom numeric type that is better suited
for the language than traditional types like u64
.
This is a low-level crate: as you’ll see below, there’s a lot of boilerplate involved in actually running a program. Stay tuned for a crate that wraps this and provides a simple CLI for running FRACTRAN programs.
This is one of Conway’s original programs, used to generate the prime numbers. They appear as the exponents of the powers of 2 that the program produces.
// make the list of fractions
let nums: Vec<u64> = vec![17, 78, 19, 23, 29,
77, 95, 77, 1, 11,
13, 15, 15, 55];
let denoms: Vec<u64> = vec![91, 85, 51, 38, 33,
29, 23, 19, 17, 13,
11, 14, 2, 1];
// Fraction is generic because it can use types other than u64, as we'll see shortly
// this is probably the easiest way to make a big list of fractio
let fracs: Vec<Fraction<u64>> = nums.into_iter()
.zip(denoms)
.map(|(num, denom)| Fraction::new(num, denom))
.collect();
// create the program from that list
let prog = Program::new(fracs);
let mut primes = vec![];
// lazy_exec() returns an iterator that runs as long as the program does, which in this case is forever
// we fix that by stopping early using take()
// note also that we feed in 2
for out in prog.lazy_exec(2).take(2000) {
// Rust's integer methods are pretty neat!
if out.is_power_of_two() {
primes.push(out.trailing_zeros());
}
}
assert_eq!(primes, vec![2, 3, 5, 7]);
You might notice that this only prints out 4 primes in 2000 executions. As it
turns out, when you can only use a single instruction, programs run long!
Additionally, the numbers can get big really quickly. u64
will overflow before
you can output 11
: try replacing .take(2000)
with a larger number to see what I
mean.
To fix the overflow issue, this crate has a PrimeBasis
type that behaves like
u64
in the ways that FRACTRAN needs, but stores the prime factorization of each
number instead of the actual value. Because of the way FRACTRAN works, it’s very
rare to use large primes, and so in practice this is a huge space improvement.
We can modify the above program pretty easily to use PrimeBasis
, which lets us
compute as many primes as we like.
let nums: Vec<u64> = vec![17, 78, 19, 23, 29,
77, 95, 77, 1, 11,
13, 15, 15, 55];
let denoms: Vec<u64> = vec![91, 85, 51, 38, 33,
29, 23, 19, 17, 13,
11, 14, 2, 1];
// we have to now create a PrimeBasis, which adds a little more boilerplate here
// try_new() returns a Result because there's a fixed number of primes that the program uses
// any number that requires a factor not in our list of primes can't be represented
// this is almost never an issue because it's very rare you want to use very large prime factors
let fracs: Vec<Fraction<PrimeBasis>> = nums.into_iter()
.zip(denoms)
.map(|(num, denom)| Fraction::new(
PrimeBasis::try_new(num).unwrap(),
PrimeBasis::try_new(denom).unwrap(),
))
.collect();
let prog = Program::new(fracs);
let mut primes = vec![];
// now can run the program for a lot longer: 100,000 iterations is pretty fast on my machine still
// note that we need to feed in a PrimeBasis because that's the type our program is in
for out_pb in prog.lazy_exec(PrimeBasis::try_new(2).unwrap())
.take(100_000) {
// we no longer have the integer methods, so we directly work with the list of exponents
if out_pb.exps[1..].iter().all(|&exp| exp == 0) {
primes.push(out_pb.exps[0]);
}
}
// we have a lot more primes now!
assert_eq!(primes, vec![2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]);
This is hardly optimized, but it isn’t slow either, especially when using
PrimeBasis
. Each operation is essentially doing some very simple iteration
over the number of prime factors being used. For programs where the largest
prime factor is small, which is most programs, a single operation is going to
be on the order of 10-20 basic operations.
The number of primes allowed in a PrimeBasis
is exported as MAX_REGS
. It’s
currently 1000
, which is a lot. (The name is because it’s easiest to think of
each prime exponent as a register in a more traditional program: this is the
number of registers.)
The list of primes used by PrimeBasis
, in order, is exported as PRIMES
: it’s
generated at compile time using MAX_REGS
and a basic sieve. Use this and
MAX_REGS
instead of hardcoding if it’s necessary.
Note that PrimeBasis
doesn’t store extra trailing zeroes in its list of
exponents, so there’s no space cost if you aren’t using the extra register
space. The reason I don’t let you use a million registers is because that
would adversely affect compilation time.