Initial WebAssembly implementation
This commit is contained in:
parent
5189a74010
commit
2793088e12
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
||||
/target
|
||||
/pkg/opentally.js
|
||||
/pkg/opentally_bg.wasm
|
||||
|
91
Cargo.lock
generated
91
Cargo.lock
generated
@ -36,12 +36,24 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@ -74,13 +86,23 @@ dependencies = [
|
||||
"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]]
|
||||
name = "crc32fast"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -111,7 +133,7 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cfg-if 1.0.0",
|
||||
"crc32fast",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
@ -192,6 +214,15 @@ version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "memchr"
|
||||
version = "2.4.0"
|
||||
@ -222,11 +253,13 @@ name = "opentally"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"console_error_panic_hook",
|
||||
"csv",
|
||||
"flate2",
|
||||
"git-version",
|
||||
"num-traits",
|
||||
"rug",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -356,6 +389,60 @@ version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
17
Cargo.toml
17
Cargo.toml
@ -4,18 +4,27 @@ version = "0.1.0"
|
||||
authors = ["Lee Yingtong Li <runassudo@yingtongli.me>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
csv = "1.1.6"
|
||||
flate2 = "1.0"
|
||||
git-version = "0.3.4"
|
||||
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"
|
||||
default-features = false
|
||||
features = ["integer", "rational", "float"]
|
||||
|
||||
[dependencies.clap]
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.clap]
|
||||
#version = "3.0.0-beta.2" # Bug 2279
|
||||
git = "https://github.com/clap-rs/clap"
|
||||
branch = "master"
|
||||
|
49
pkg/test.html
Normal file
49
pkg/test.html
Normal 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>
|
@ -19,7 +19,6 @@ use crate::logger::Logger;
|
||||
use crate::numbers::Number;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufRead, Lines};
|
||||
|
||||
pub struct Election<N> {
|
||||
pub seats: usize,
|
||||
@ -28,9 +27,9 @@ pub struct 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
|
||||
let line = lines.next().expect("Unexpected EOF").expect("IO Error");
|
||||
let line = lines.next().expect("Unexpected EOF");
|
||||
let mut bits = line.split(" ");
|
||||
let num_candidates = 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
|
||||
for line in &mut lines {
|
||||
let line = line.expect("IO Error");
|
||||
if line == "0" {
|
||||
break;
|
||||
}
|
||||
@ -69,7 +67,7 @@ impl<N: Number> Election<N> {
|
||||
|
||||
// Read 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("\"") {
|
||||
line = &line[1..line.len()-1];
|
||||
}
|
||||
|
@ -92,10 +92,10 @@ fn main() {
|
||||
|
||||
// Create and count election according to --numbers
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
@ -16,17 +16,22 @@
|
||||
*/
|
||||
|
||||
mod native;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod rational;
|
||||
|
||||
use num_traits::{NumAssignRef, NumRef};
|
||||
use rug::{self, Assign};
|
||||
|
||||
use std::cmp::Ord;
|
||||
use std::fmt;
|
||||
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 + 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 from(n: usize) -> Self;
|
||||
|
||||
@ -42,4 +47,6 @@ pub trait Number: NumRef + NumAssignRef + ops::Neg<Output=Self> + Ord + Assign +
|
||||
}
|
||||
|
||||
pub use self::native::NativeFloat64;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use self::rational::Rational;
|
||||
|
@ -15,10 +15,9 @@
|
||||
* 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 rug::Assign;
|
||||
|
||||
use std::cmp::{Ord, Ordering, PartialEq, PartialOrd};
|
||||
use std::num::ParseIntError;
|
||||
|
@ -15,10 +15,11 @@
|
||||
* 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 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::fmt;
|
||||
|
@ -17,6 +17,9 @@
|
||||
|
||||
#![allow(mutable_borrow_reservation_conflict)]
|
||||
|
||||
//#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm;
|
||||
|
||||
use crate::numbers::Number;
|
||||
use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote};
|
||||
|
||||
|
156
src/stv/wasm.rs
Normal file
156
src/stv/wasm.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ fn aec_tas19_rational() {
|
||||
let lines = gz_reader.lines();
|
||||
|
||||
// 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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user