Numerical I/O

Systems in Rust

Author

Prof. Calvin

Announcements

  • Scaffolding assignment for ix
    • Big numbers are hard to type, think of, reason about, etc.
    • Hard to check your work.

Homework

  • ix beckons
  • Due Friday, 24 Oct. at 1440 ET.
    • You will probably need to finish this to do that.
  • ix vs. eg. i32, i64, can store infinite values, and your job is to make it.

Today

Struct

  • Before anything else, have to find a way to make an ix.
    • I am granting you full freedom to implement ix as you see fit with one exception:
    • You must implement it yourself, not use an existing “BigNum” or “BigInt” crate, like [this])(https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html)
  • Personally, I just used a sign boolean and a vector of unsigned values. You are welcome to take this approach:
src/lib.rs
#![allow(non_camel_case_types)]
pub struct ix {
    sign: bool,
    vals: Vec<u64>,
}
  • The real merits of showing this are:
    • You can use #![allow(non_camel_case_types)] to avoid the annoying warning for a lower case name.
      • Naming a data structure in lower case is poor form (consider String vs. &str) but…
      • I was out of ideas.
  • The structure is public, so can be used from a testing src/main.rs.
  • The internal fields are not public, so you can make any changes you like within src/lib.rs.

Test it

  • You can test the same way we tested f16:
src/main.rs
fn main() {
    let x : f16::f16;
}
  • The first f16 is the crate name, the second is the type name.

Aside

  • For my money, f16::f16 looks terrible.
src/main.rs
use f16::*;

fn main() {
    let x : f16;
}

Aside

  • Rust admonished you for using f16 instead of F16.
warning: type `f16` should have an upper camel case name
 --> src/lib.rs:1:12
  |
1 | pub struct f16 {
  |            ^^^ help: convert the identifier to upper camel case (notice the capitalization): `F16`
  |
  = note: `#[warn(non_camel_case_types)]` on by default
  • Everyone is a critic!
  • #![allow(non_camel_case_types)]

Aside

Input/Output

  • Versus built-in types, we’ll just provide a ways to get values in and out.
  • Vs. f16, this won’t be easy in the sense that we are explicitly trying to work with values that do not fit into any existing numerical type.
  • For example, I will provide the following function, but it isn’t particularly helpful.
src/main.rs
pub fn u64_to_ix(val: u64) -> ix {
    return ix {
        sign: false,
        vals: vec![val],
    };
}
  • The real motivation for todays lab is to use a testing framework.
  • I am providing mine, which you are not required to use but that I quite like.
    • Values are input as command line arguments as hex strings, with a 0x prefix.
      • I didn’t check if I can input negative values, content to create negatives via subtraction rather than define a standard.
    • Values are output to stdout as hex values with no 0x prefix but with a sign.
      • This was so I could read them directly with Python int() for testing.
    • Operations are specified using a capitalized, 3 letter abbreviation describing one of the four supported operations:
      • ADD, SUB, MUL, DIV, REM - which I want to reiterate is four operations.
      • This should say “pattern matching” to you.
  • I’ll present the components of the testing framework, and your task is to convince yourself you understand them.

src/main.rs

  • Main:
    • Uses the ix implementation from src/lib.rs
    • Reads command line arguments generated by tester.py
    • Converts two command line strings representing numerical hex vlaues into ix instances.
    • Captures the final command line argument to determine which operation to perform.
    • Calls the appropriate ix arithmetic function and captures its return value.
      • I call these <op>_ix for op in add, sub, mul, div, rem
    • Prints to stdout the returned ix as a hex string.
  • While intended to be used by tester.py, it can also be used manually!
$ cargo run 0x10 0x20 ADD
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/bignum 0x10 0x20 ADD`
0000000000000030
  • I do not add a newline after this value.
  • You can, and probably should want to, make your own src/main.rs that does these things, but you are not required to do so.
  • I am providing mine as a “spoiler marked” HTML <details> block.

My src/main.rs

src/main.rs
use bignum::*;

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let a = h2i_ix(&args[1]);
    let b = h2i_ix(&args[2]);
    match args[3].as_str() {
        "ADD" => see_ix(&add_ix(&a, &b)),
        "SUB" => see_ix(&sub_ix(&a, &b)),
        "MUL" => todo!(),
        "DIV" => todo!(),
        "REM" => todo!(),
        &_    => println!("Operator not recognized: choose from ADD, SUB, MUL, DIV, REM"),
    }
}

tester.py

  • Tester:
    • Using Python’s built-in integers, generates two large random numbers, I generated mine to be between 500 and 512 bits.
    • Convert them to hex strings.
    • For each of the debatebly four or five operations, the tester:
      • Uses subprocess to dispatch cargo to run with three command line arguments: the two hex strings and the operation (in all caps).
      • Captures the output of the process.
      • Computes the same operation using Python arithmetic operations.
      • Uses naive string comparison to compare the two values.
        • Optionally raises a debug message in the event of a difference.
  • To support tester usage, I will provide:
    • The tester.py code
    • An example run on a partial ix implementation, with only addition and subtraction completed.
  • I am spoiler marking both, in case you want to make your own.

My tester.py

tester.py
DEBUG = 0
CMD = "cargo run --"

import subprocess, os, random
from operator import add, sub, mul, floordiv as quo, mod as rem

bigone, bigtwo = random.randint(2 ** 500, 2 ** 512), random.randint(2 ** 500, 2 ** 512)
hexone, hextwo = hex(bigone), hex(bigtwo)
DEBUG and print("\nhexone =\n", hexone, "\nhextwo = \n", hextwo)

from operator import add, sub, mul, floordiv as quo, mod as rem
ops = {'ADD':add,'SUB':sub,'MUL':mul,'QUO':quo,'REM':rem}
for op in ops:
    result = int(subprocess.check_output(["cargo", "run", hexone, hextwo, op]),16)
    answer = ops[op](bigone,bigtwo)
    if result != answer:
        print("Operator", op, "failed.")
        DEBUG and print("Expected:")
        DEBUG and print(hex(answer))
        DEBUG and print("Received:")
        DEBUG and print(hex(result))
        exit()
    else:
        print(op, "passes.")

Example output

  • Note the panic caused by Rust todo!() and the separate, Python subprocess.CalledProcessError after the failure propagates back to the calling script.
$ python3 tester.py
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/bignum 0x2c2e4c34f428560aedbee82a9a7ca5a7071ef9d9b3b23834ef1ce63be90e052e94d1411de3c1191fdb1ebfd39fde41bbfc8b95e3faeae64a0fe21d50b9ce53d8 0x58010ed08d2415a81a17e369a3e00443d922d0219dd66c1b74473511140e1f4f24450840e9e1a7c4bd4cac368d30a4ba5aa075fb65ec92a714c7f73b42122bb0 ADD`
ADD passes.
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/bignum 0x2c2e4c34f428560aedbee82a9a7ca5a7071ef9d9b3b23834ef1ce63be90e052e94d1411de3c1191fdb1ebfd39fde41bbfc8b95e3faeae64a0fe21d50b9ce53d8 0x58010ed08d2415a81a17e369a3e00443d922d0219dd66c1b74473511140e1f4f24450840e9e1a7c4bd4cac368d30a4ba5aa075fb65ec92a714c7f73b42122bb0 SUB`
SUB passes.
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/bignum 0x2c2e4c34f428560aedbee82a9a7ca5a7071ef9d9b3b23834ef1ce63be90e052e94d1411de3c1191fdb1ebfd39fde41bbfc8b95e3faeae64a0fe21d50b9ce53d8 0x58010ed08d2415a81a17e369a3e00443d922d0219dd66c1b74473511140e1f4f24450840e9e1a7c4bd4cac368d30a4ba5aa075fb65ec92a714c7f73b42122bb0 MUL`

thread 'main' panicked at src/main.rs:10:18:
not yet implemented
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Traceback (most recent call last):
  File "/home/user/tmp/ix/tester.py", line 19, in <module>
    result = int(subprocess.check_output(["cargo", "run", hexone, hextwo, op]),16)
  File "/usr/lib/python3.10/subprocess.py", line 421, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/usr/lib/python3.10/subprocess.py", line 526, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['cargo', 'run', '0x2c2e4c34f428560aedbee82a9a7ca5a7071ef9d9b3b23834ef1ce63be90e052e94d1411de3c1191fdb1ebfd39fde41bbfc8b95e3faeae64a0fe21d50b9ce53d8', '0x58010ed08d2415a81a17e369a3e00443d922d0219dd66c1b74473511140e1f4f24450840e9e1a7c4bd4cac368d30a4ba5aa075fb65ec92a714c7f73b42122bb0', 'MUL']' returned non-zero exit status 101.

Next Steps

  • Implement addition and subtraction (you will likely want to do both at the same time) and test them.
  • For a hint, I wrote the following helper functions:
// Helpers: Add/sub magnitudes (absolute values) of two numbers.
// "aug" and "add" are short for "augend" and "addend"
fn add_mag(aug_vals: &Vec<u64>, add_vals: &Vec<u64>) -> Vec<u64> 
// "min" and "sub" are short for "minuend" and "subtrahend"
fn sub_mag(min_vals: &Vec<u64>, sub_vals: &Vec<u64>) -> Vec<u64> 
// Compute the "greater than or equal" between two values.
fn gte_mag(a_vals: &Vec<u64>, b_vals: &Vec<u64>) -> bool 
  • I am also providing my entire sub_ix, again spoiler marked.
pub fn sub_ix(a: &ix, b: &ix) -> ix {
    let b = ix {
        sign: !b.sign,
        vals: b.vals.clone(),
    };
    return add_ix(&a, &b);
}