Initial WebAssembly implementation

This commit is contained in:
RunasSudo 2021-05-30 18:28:39 +10:00
parent 5189a74010
commit 2793088e12
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
12 changed files with 331 additions and 20 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
/target /target
/pkg/opentally.js
/pkg/opentally_bg.wasm

91
Cargo.lock generated
View File

@ -36,12 +36,24 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bumpalo"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.4.3" version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -74,13 +86,23 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "console_error_panic_hook"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211"
dependencies = [
"cfg-if 0.1.10",
"wasm-bindgen",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [ dependencies = [
"cfg-if", "cfg-if 1.0.0",
] ]
[[package]] [[package]]
@ -111,7 +133,7 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if 1.0.0",
"crc32fast", "crc32fast",
"libc", "libc",
"miniz_oxide", "miniz_oxide",
@ -192,6 +214,15 @@ version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if 1.0.0",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.0" version = "2.4.0"
@ -222,11 +253,13 @@ name = "opentally"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"console_error_panic_hook",
"csv", "csv",
"flate2", "flate2",
"git-version", "git-version",
"num-traits", "num-traits",
"rug", "rug",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -356,6 +389,60 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wasm-bindgen"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -4,18 +4,27 @@ version = "0.1.0"
authors = ["Lee Yingtong Li <runassudo@yingtongli.me>"] authors = ["Lee Yingtong Li <runassudo@yingtongli.me>"]
edition = "2018" edition = "2018"
[lib]
crate-type = ["lib", "cdylib"]
[dependencies] [dependencies]
csv = "1.1.6"
flate2 = "1.0"
git-version = "0.3.4" git-version = "0.3.4"
num-traits = "0.2" num-traits = "0.2"
wasm-bindgen = "0.2.74"
[dependencies.rug] # Only for WebAssembly - include here for syntax highlighting
console_error_panic_hook = "0.1.6"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
csv = "1.1.6"
flate2 = "1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.rug]
version = "1.12" version = "1.12"
default-features = false default-features = false
features = ["integer", "rational", "float"] features = ["integer", "rational", "float"]
[dependencies.clap] [target.'cfg(not(target_arch = "wasm32"))'.dependencies.clap]
#version = "3.0.0-beta.2" # Bug 2279 #version = "3.0.0-beta.2" # Bug 2279
git = "https://github.com/clap-rs/clap" git = "https://github.com/clap-rs/clap"
branch = "master" branch = "master"

49
pkg/test.html Normal file
View File

@ -0,0 +1,49 @@
<html>
<body>
<div id="output" style="white-space: pre-wrap;"></div>
<script src="opentally.js"></script>
<script>
var election_data = '4 2\n\
3 1 3 4 0\n\
4 1 3 2 0\n\
2 4 1 3 0\n\
1 2 0\n\
2 2 4 3 1 0\n\
1 3 4 2 0\n\
0\n\
"Adam"\n\
"Basil"\n\
"Charlotte"\n\
"Donald"\n\
"Title"';
console.log = function(v) {
document.getElementById('output').append(v);
document.getElementById('output').append("\n");
};
var { CountStateFloat64, STVOptions, count_init_float64, count_one_stage_float64, election_from_blt_float64, make_and_print_result_float64 } = wasm_bindgen;
async function run() {
await wasm_bindgen("opentally_bg.wasm");
let stv_opts = STVOptions.new(2, "one_round");
let election = election_from_blt_float64(election_data);
let state = CountStateFloat64.new(election);
count_init_float64(state, stv_opts);
make_and_print_result_float64(1, state);
for (let stage_num = 2;; stage_num++) {
let is_done = count_one_stage_float64(state, stv_opts);
if (is_done) {
break;
}
make_and_print_result_float64(stage_num, state);
}
}
run();
</script>
</body>
</html>

View File

@ -19,7 +19,6 @@ use crate::logger::Logger;
use crate::numbers::Number; use crate::numbers::Number;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{BufRead, Lines};
pub struct Election<N> { pub struct Election<N> {
pub seats: usize, pub seats: usize,
@ -28,9 +27,9 @@ pub struct Election<N> {
} }
impl<N: Number> Election<N> { impl<N: Number> Election<N> {
pub fn from_blt<B: BufRead>(mut lines: Lines<B>) -> Self { pub fn from_blt<I: Iterator<Item=String>>(mut lines: I) -> Self {
// Read first line // Read first line
let line = lines.next().expect("Unexpected EOF").expect("IO Error"); let line = lines.next().expect("Unexpected EOF");
let mut bits = line.split(" "); let mut bits = line.split(" ");
let num_candidates = bits.next().expect("Syntax Error").parse().expect("Syntax Error"); let num_candidates = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
let seats: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error"); let seats: usize = bits.next().expect("Syntax Error").parse().expect("Syntax Error");
@ -44,7 +43,6 @@ impl<N: Number> Election<N> {
// Read ballots // Read ballots
for line in &mut lines { for line in &mut lines {
let line = line.expect("IO Error");
if line == "0" { if line == "0" {
break; break;
} }
@ -69,7 +67,7 @@ impl<N: Number> Election<N> {
// Read candidates // Read candidates
for line in lines.take(num_candidates) { for line in lines.take(num_candidates) {
let mut line = &line.expect("IO Error")[..]; let mut line = &line[..];
if line.starts_with("\"") && line.ends_with("\"") { if line.starts_with("\"") && line.ends_with("\"") {
line = &line[1..line.len()-1]; line = &line[1..line.len()-1];
} }

View File

@ -92,10 +92,10 @@ fn main() {
// Create and count election according to --numbers // Create and count election according to --numbers
if cmd_opts.numbers == "rational" { if cmd_opts.numbers == "rational" {
let election: Election<Rational> = Election::from_blt(lines); let election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
count_election(election, cmd_opts); count_election(election, cmd_opts);
} else if cmd_opts.numbers == "float64" { } else if cmd_opts.numbers == "float64" {
let election: Election<NativeFloat64> = Election::from_blt(lines); let election: Election<NativeFloat64> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
count_election(election, cmd_opts); count_election(election, cmd_opts);
} }
} }

View File

@ -16,17 +16,22 @@
*/ */
mod native; mod native;
#[cfg(not(target_arch = "wasm32"))]
mod rational; mod rational;
use num_traits::{NumAssignRef, NumRef}; use num_traits::{NumAssignRef, NumRef};
use rug::{self, Assign};
use std::cmp::Ord; use std::cmp::Ord;
use std::fmt; use std::fmt;
use std::ops; use std::ops;
pub trait Assign<Src=Self> {
fn assign(&mut self, src: Src);
}
//pub trait Number: NumRef + NumAssignRef + PartialOrd + Assign + Clone + fmt::Display where for<'a> &'a Self: RefNum<&'a Self> { //pub trait Number: NumRef + NumAssignRef + PartialOrd + Assign + Clone + fmt::Display where for<'a> &'a Self: RefNum<&'a Self> {
pub trait Number: NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self>{ pub trait Number: NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign + Clone + fmt::Display where for<'a> Self: Assign<&'a Self> {
fn new() -> Self; fn new() -> Self;
fn from(n: usize) -> Self; fn from(n: usize) -> Self;
@ -42,4 +47,6 @@ pub trait Number: NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign +
} }
pub use self::native::NativeFloat64; pub use self::native::NativeFloat64;
#[cfg(not(target_arch = "wasm32"))]
pub use self::rational::Rational; pub use self::rational::Rational;

View File

@ -15,10 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use super::Number; use super::{Assign, Number};
use num_traits::{Num, One, Zero}; use num_traits::{Num, One, Zero};
use rug::Assign;
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
use std::num::ParseIntError; use std::num::ParseIntError;

View File

@ -15,10 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use super::Number; use super::{Assign, Number};
use num_traits::{Num, One, Zero}; use num_traits::{Num, One, Zero};
use rug::{self, Assign, ops::Pow, rational::ParseRationalError}; use rug::{self, ops::Pow, rational::ParseRationalError};
use rug::Assign as RugAssign;
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd}; use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
use std::fmt; use std::fmt;

View File

@ -17,6 +17,9 @@
#![allow(mutable_borrow_reservation_conflict)] #![allow(mutable_borrow_reservation_conflict)]
//#[cfg(target_arch = "wasm32")]
pub mod wasm;
use crate::numbers::Number; use crate::numbers::Number;
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};

156
src/stv/wasm.rs Normal file
View File

@ -0,0 +1,156 @@
/* OpenTally: Open-source election vote counting
* Copyright © 2021 Lee Yingtong Li (RunasSudo)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::election::{Candidate, CandidateState, CountCard, CountState, CountStateOrRef, Election, StageResult};
use crate::numbers::{NativeFloat64, Number};
use crate::stv;
extern crate console_error_panic_hook;
use wasm_bindgen::prelude::wasm_bindgen;
// Logging
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
macro_rules! cprintln {
($($t:tt)*) => (
#[allow(unused_unsafe)]
unsafe { log(&format_args!($($t)*).to_string()) }
)
}
// Exported functions
#[wasm_bindgen]
pub fn election_from_blt_float64(text: String) -> ElectionFloat64 {
// Install panic! hook
console_error_panic_hook::set_once();
let election: Election<NativeFloat64> = Election::from_blt(text.lines().map(|s| s.to_string()).into_iter());
return ElectionFloat64(election);
}
#[wasm_bindgen]
pub fn count_init_float64(state: &mut CountStateFloat64, opts: &STVOptions) {
stv::count_init(&mut state.0, &opts.0);
}
#[wasm_bindgen]
pub fn count_one_stage_float64(state: &mut CountStateFloat64, opts: &STVOptions) -> bool {
return stv::count_one_stage(&mut state.0, &opts.0);
}
// Reporting
fn print_candidates<'a, N: 'a + Number, I: Iterator<Item=(&'a Candidate, &'a CountCard<'a, N>)>>(candidates: I) {
for (candidate, count_card) in candidates {
if count_card.state == CandidateState::ELECTED {
cprintln!("- {}: {:.dps$} ({:.dps$}) - ELECTED {}", candidate.name, count_card.votes, count_card.transfers, count_card.order_elected, dps=2);
} else if count_card.state == CandidateState::EXCLUDED {
cprintln!("- {}: {:.dps$} ({:.dps$}) - Excluded {}", candidate.name, count_card.votes, count_card.transfers, -count_card.order_elected, dps=2);
} else {
cprintln!("- {}: {:.dps$} ({:.dps$})", candidate.name, count_card.votes, count_card.transfers, dps=2);
}
}
}
fn print_stage<N: Number>(stage_num: usize, result: &StageResult<N>) {
// Print stage details
match result.kind {
None => { cprintln!("{}. {}", stage_num, result.title); }
Some(kind) => { cprintln!("{}. {} {}", stage_num, kind, result.title); }
};
cprintln!("{}", result.logs.join(" "));
let state = result.state.as_ref();
// Print candidates
let candidates = state.election.candidates.iter()
.map(|c| (c, state.candidates.get(c).unwrap()));
print_candidates(candidates);
// Print summary rows
cprintln!("Exhausted: {:.dps$} ({:.dps$})", state.exhausted.votes, state.exhausted.transfers, dps=2);
cprintln!("Loss by fraction: {:.dps$} ({:.dps$})", state.loss_fraction.votes, state.loss_fraction.transfers, dps=2);
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
total_vote += &state.exhausted.votes;
total_vote += &state.loss_fraction.votes;
cprintln!("Total votes: {:.dps$}", total_vote, dps=2);
cprintln!("Quota: {:.dps$}", state.quota, dps=2);
cprintln!("");
}
#[wasm_bindgen]
pub fn make_and_print_result_float64(stage_num: usize, state: &CountStateFloat64) {
let result = StageResult {
kind: state.0.kind,
title: &state.0.title,
logs: state.0.logger.render(),
state: CountStateOrRef::from(&state.0),
};
print_stage(stage_num, &result);
}
// Wrapper structs
// Required as we cannot specify &'static in wasm-bindgen: issue #1187
#[wasm_bindgen]
pub struct CountStateFloat64(CountState<'static, NativeFloat64>);
#[wasm_bindgen]
impl CountStateFloat64 {
pub fn new(election: &ElectionFloat64) -> Self {
return CountStateFloat64(CountState::new(election.as_static()));
}
}
#[wasm_bindgen]
pub struct ElectionFloat64(Election<NativeFloat64>);
#[wasm_bindgen]
impl ElectionFloat64 {
pub fn seats(&self) -> usize { self.0.seats }
fn as_static(&self) -> &'static Election<NativeFloat64> {
// Need to have this as we cannot specify &'static in wasm-bindgen: issue #1187
unsafe {
let ptr = &self.0 as *const Election<NativeFloat64>;
&*ptr
}
}
}
#[wasm_bindgen]
pub struct STVOptions(stv::STVOptions<'static>);
#[wasm_bindgen]
impl STVOptions {
pub fn new(round_votes: Option<usize>, exclusion: String) -> Self {
if exclusion == "one_round" {
return STVOptions(stv::STVOptions {
round_votes: round_votes,
exclusion: &"one_round",
});
} else {
panic!("Unknown --exclusion");
}
}
}

View File

@ -48,7 +48,7 @@ fn aec_tas19_rational() {
let lines = gz_reader.lines(); let lines = gz_reader.lines();
// Read BLT // Read BLT
let election: Election<Rational> = Election::from_blt(lines); let election: Election<Rational> = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
// TODO: Validate candidate names // TODO: Validate candidate names