Repository: Sunscreen-tech/hpu
Pull request #24: Add signed integer support with a multiplication stub; add unit tests
Author: login: cedricjiang
Created: 2025-04-14T16:59:25Z | Merged: 2025-04-15T18:27:41Z



Diff:
diff --git a/parasol_runtime/src/circuits/mul.rs b/parasol_runtime/src/circuits/mul.rs
index 7487b458..31503ae7 100644
--- a/parasol_runtime/src/circuits/mul.rs
+++ b/parasol_runtime/src/circuits/mul.rs
@@ -12,6 +12,15 @@ use crate::{
     fluent::Muxable,
 };
 
+/// Compute the product of 2 N-bit unsigned values a and b.
+pub fn append_int_multiply<OutCt: Muxable>(
+    uop_graph: &mut FheCircuit,
+    a: &[NodeIndex],
+    b: &[NodeIndex],
+) -> (Vec<NodeIndex>, Vec<NodeIndex>) {
+    todo!("append_int_multiply ({:#?}, {:#?}, {:#?})", uop_graph, a, b);
+}
+
 /// Compute the product of 2 N-bit unsigned values a and b.
 pub fn append_uint_multiply<OutCt: Muxable>(
     uop_graph: &mut FheCircuit,
diff --git a/parasol_runtime/src/fluent/generic_int.rs b/parasol_runtime/src/fluent/generic_int.rs
index 53a55c6d..2a46b80a 100644
--- a/parasol_runtime/src/fluent/generic_int.rs
+++ b/parasol_runtime/src/fluent/generic_int.rs
@@ -20,8 +20,11 @@ use petgraph::stable_graph::NodeIndex;
 use serde::{Deserialize, Serialize};
 use sunscreen_tfhe::entities::Polynomial;
 
-/// Marker trait for distinguishing unsigned and signed integer types
-pub trait Sign {}
+/// Trait for distinguishing unsigned and signed integer types
+pub trait Sign {
+    /// Compare circuit generation function for this sign
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> MuxCircuit;
+}
 
 /// A collection of graph nodes resulting from FHE operations over generic integers (e.g. the
 /// result of adding two 7-bit values).
@@ -177,16 +180,7 @@ impl<const N: usize, U: Sign> GenericIntGraphNodes<'_, N, L1GlweCiphertext, U> {
     }
 }
 
-/// Trait to allow the generic comparator functionality to be implemented in specialized versions
-pub trait GeneratesCompareCircuit {
-    /// Circuit generation function
-    fn gen_compare_circuit(&self, max_len: usize, gt: bool, eq: bool) -> MuxCircuit;
-}
-
-impl<'a, const N: usize, V: Sign> GenericIntGraphNodes<'a, N, L1GgswCiphertext, V>
-where
-    Self: GeneratesCompareCircuit,
-{
+impl<'a, const N: usize, V: Sign> GenericIntGraphNodes<'a, N, L1GgswCiphertext, V> {
     pub(crate) fn cmp<const M: usize, OutCt: Muxable>(
         &self,
         other: &GenericIntGraphNodes<M, L1GgswCiphertext, V>,
@@ -195,7 +189,7 @@ where
         eq: bool,
     ) -> BitNode<OutCt> {
         let max_len = M.max(N);
-        let mux_circuit = self.gen_compare_circuit(max_len, gt, eq);
+        let mux_circuit = V::gen_compare_circuit(max_len, gt, eq);
 
         let zero = ctx.circuit.borrow_mut().add_node(FheOp::ZeroGgsw1);
 
diff --git a/parasol_runtime/src/fluent/int.rs b/parasol_runtime/src/fluent/int.rs
new file mode 100644
index 00000000..80b78592
--- /dev/null
+++ b/parasol_runtime/src/fluent/int.rs
@@ -0,0 +1,194 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        // add 1 for the sign bit that gets removed in `take`, note the minus 1 in min_len
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
+
+        let sign_bit = self.bits.last().unwrap();
+
+        let iter = self
+            .bits
+            .iter()
+            .copied()
+            .take(min_len)
+            .chain((0..extend).map(|_| sign_bit.to_owned()));
+
+        IntGraphNodes::from_bit_nodes(iter, &ctx.allocator)
+    }
+}
+
+impl<'a, const N: usize> IntGraphNodes<'a, N, L1GgswCiphertext> {
+    /// Compute `self * other`.
+    ///
+    /// # Remarks
+    /// Requires `self` and `other` to be [`L1GgswCiphertext`]s. Use [`Self::convert`] to
+    /// change to this type.
+    pub fn mul<OutCt: Muxable>(
+        &self,
+        other: &Self,
+        ctx: &'a FheCircuitCtx,
+    ) -> IntGraphNodes<'a, N, OutCt> {
+        let a = self.bits.iter().map(|x| x.node).collect::<Vec<_>>();
+
+        let b = other.bits.iter().map(|x| x.node).collect::<Vec<_>>();
+
+        let (lo, _hi) = append_int_multiply::<OutCt>(&mut ctx.circuit.borrow_mut(), &a, &b);
+
+        // TODO: prune the high bits somehow?
+
+        IntGraphNodes::from_nodes(lo.into_iter(), &ctx.allocator)
+    }
+}
+
+/// Signed variant for [`PackedGenericIntGraphNode`]
+pub type PackedIntGraphNode<const N: usize, T> = PackedGenericIntGraphNode<N, T, Signed>;
+
+/// Ssigned variant for [`GenericInt`]
+pub type Int<const N: usize, T> = GenericInt<N, T, Signed>;
+
+/// Signed variant for [`PackedGenericInt`]
+pub type PackedInt<const N: usize, T> = PackedGenericInt<N, T, Signed>;
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        DEFAULT_128, L0LweCiphertext, L1GlevCiphertext, L1GlweCiphertext, L1LweCiphertext,
+        crypto::PublicKey,
+        test_utils::{get_encryption_128, get_public_key_128, get_secret_keys_128, make_uproc_128},
+    };
+    use serde::{Deserialize, Serialize};
+
+    use super::*;
+
+    #[test]
+    fn can_roundtrip_packed_int() {
+        let enc = get_encryption_128();
+
+        let sk = get_secret_keys_128();
+        let pk = get_public_key_128();
+
+        let val = PackedInt::<16, L1GlweCiphertext>::encrypt(2u64.pow(16) - 42, &enc, &pk);
+
+        assert_eq!(val.decrypt(&enc, &sk), 2u64.pow(16) - 42);
+    }
+
+    #[test]
+    fn can_unpack_int() {
+        let enc = get_encryption_128();
+
+        let sk = get_secret_keys_128();
+        let pk = get_public_key_128();
+        let (uproc, fc) = make_uproc_128();
+
+        let val = PackedInt::<16, L1GlweCiphertext>::encrypt(2u64.pow(16) - 42, &enc, &pk);
+
+        let ctx = FheCircuitCtx::new();
+
+        let as_unpacked = val
+            .graph_input(&ctx)
+            .unpack(&ctx)
+            .collect_outputs(&ctx, &enc);
+
+        uproc
+            .lock()
+            .unwrap()
+            .run_graph_blocking(&ctx.circuit.borrow(), &fc);
+
+        assert_eq!(as_unpacked.decrypt(&enc, &sk), 2u64.pow(16) - 42);
+    }
+
+    #[test]
+    fn can_pack_int() {
+        let enc = get_encryption_128();
+        let sk = get_secret_keys_128();
+        let (uproc, fc) = make_uproc_128();
+
+        let val = Int::<15, L1GlweCiphertext>::encrypt_secret(2u64.pow(15) - 42, &enc, &sk);
+
+        let ctx = FheCircuitCtx::new();
+
+        let actual = val
+            .graph_inputs(&ctx)
+            .pack(&ctx, &enc)
+            .collect_output(&ctx, &enc);
+
+        uproc
+            .lock()
+            .unwrap()
+            .run_graph_blocking(&ctx.circuit.borrow(), &fc);
+
+        assert_eq!(actual.decrypt(&enc, &sk), 2u64.pow(15) - 42);
+    }
+
+    #[test]
+    fn can_safe_deserialize_int() {
+        fn case<T: CiphertextOps + for<'a> Deserialize<'a> + Serialize>() {
+            let enc = get_encryption_128();
+            let sk = get_secret_keys_128();
+
+            let val = Int::<15, T>::encrypt_secret(2u64.pow(15) - 42, &enc, &sk);
+
+            let ser = bincode::serialize(&val).unwrap();
+            crate::safe_bincode::deserialize::<Int<15, T>>(&ser, &DEFAULT_128).unwrap();
+        }
+
+        case::<L0LweCiphertext>();
+        case::<L1LweCiphertext>();
+        case::<L1GlweCiphertext>();
+        case::<L1GlevCiphertext>();
+    }
+
+    #[test]
+    fn can_safe_deserialize_packed_int() {
+        let enc = get_encryption_128();
+        let sk = get_secret_keys_128();
+        let pk = PublicKey::generate(&DEFAULT_128, &sk);
+
+        let val = PackedInt::<15, L1GlweCiphertext>::encrypt(2u64.pow(15) - 42, &enc, &pk);
+
+        let ser = bincode::serialize(&val).unwrap();
+        crate::safe_bincode::deserialize::<PackedInt<15, L1GlweCiphertext>>(&ser, &DEFAULT_128)
+            .unwrap();
+    }
+
+    #[test]
+    fn can_trivial_encrypt_packed_int() {
+        let enc = get_encryption_128();
+        let sk = get_secret_keys_128();
+
+        let val = PackedInt::<15, L1GlweCiphertext>::trivial_encrypt(2u64.pow(15) - 42, &enc);
+
+        assert_eq!(val.decrypt(&enc, &sk), 2u64.pow(15) - 42);
+    }
+}
diff --git a/parasol_runtime/src/fluent/mod.rs b/parasol_runtime/src/fluent/mod.rs
index 6cbe7810..cb3b8951 100644
--- a/parasol_runtime/src/fluent/mod.rs
+++ b/parasol_runtime/src/fluent/mod.rs
@@ -18,10 +18,12 @@ use crate::{
 
 mod bit;
 mod generic_int;
+mod int;
 mod uint;
 
 pub use bit::*;
 pub use generic_int::*;
+pub use int::*;
 pub use uint::*;
 
 /// A context for building FHE circuits out of high-level primitives (e.g.
@@ -360,6 +362,7 @@ impl Muxable for L1GlevCiphertext {
 #[cfg(test)]
 mod tests {
     use bit::Bit;
+    use generic_int::GenericInt;
     use rand::{RngCore, thread_rng};
     use uint::UInt;
 
@@ -373,14 +376,14 @@ mod tests {
 
     use super::*;
 
-    fn roundtrip<T: CiphertextOps>() {
+    fn roundtrip<T: CiphertextOps, U: Sign>() {
         let sk = get_secret_keys_80();
         let enc = Encryption::new(&DEFAULT_80);
 
         for _ in 0..32 {
             // Make 16-bit integers.
             let val = thread_rng().next_u64() % 0x10000;
-            let ct = UInt::<16, T>::encrypt_secret(val, &enc, &sk);
+            let ct = GenericInt::<16, T, U>::encrypt_secret(val, &enc, &sk);
             let actual = ct.decrypt(&enc, &sk);
 
             assert_eq!(val, actual);
@@ -389,30 +392,37 @@ mod tests {
 
     #[test]
     fn can_roundtrip_l0_lwe() {
-        roundtrip::<L0LweCiphertext>();
+        roundtrip::<L0LweCiphertext, Unsigned>();
+        roundtrip::<L0LweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_lwe() {
-        roundtrip::<L1LweCiphertext>();
+        roundtrip::<L1LweCiphertext, Unsigned>();
+        roundtrip::<L1LweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_glwe() {
-        roundtrip::<L1GlweCiphertext>();
+        roundtrip::<L1GlweCiphertext, Unsigned>();
+        roundtrip::<L1GlweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_ggsw() {
-        roundtrip::<L1GgswCiphertext>();
+        roundtrip::<L1GgswCiphertext, Unsigned>();
+        roundtrip::<L1GgswCiphertext, Signed>();
     }
 
-    fn input_output<T: CiphertextOps>() {
+    fn input_output<T: CiphertextOps, U: Sign>(test_val: u64) {
         let (uproc, fc) = make_uproc_80();
         let enc = get_encryption_80();
 
-        let input =
-            UInt::<16, T>::encrypt_secret(1234, &get_encryption_80(), &get_secret_keys_80());
+        let input = GenericInt::<16, T, U>::encrypt_secret(
+            test_val,
+            &get_encryption_80(),
+            &get_secret_keys_80(),
+        );
 
         let graph = FheCircuitCtx::new();
 
@@ -425,38 +435,42 @@ mod tests {
             .run_graph_blocking(&graph.circuit.borrow(), &fc);
 
         let actual = output.decrypt(&enc, &get_secret_keys_80());
-        assert_eq!(actual, 1234);
+        assert_eq!(actual, test_val);
     }
 
     #[test]
-    fn can_input_output_uint_graph_l0_lwe() {
-        input_output::<L0LweCiphertext>();
+    fn can_input_output_generic_int_graph_l0_lwe() {
+        input_output::<L0LweCiphertext, Unsigned>(1234);
+        input_output::<L0LweCiphertext, Signed>(65432);
     }
 
     #[test]
-    fn can_input_output_uint_graph_l1_lwe() {
-        input_output::<L1LweCiphertext>();
+    fn can_input_output_generic_int_graph_l1_lwe() {
+        input_output::<L1LweCiphertext, Unsigned>(1234);
+        input_output::<L1LweCiphertext, Signed>(65432);
     }
 
     #[test]
-    fn can_input_output_uint_graph_l1_ggsw() {
-        input_output::<L1GgswCiphertext>();
+    fn can_input_output_generic_int_graph_l1_ggsw() {
+        input_output::<L1GgswCiphertext, Unsigned>(1234);
+        input_output::<L1GgswCiphertext, Signed>(65432);
     }
 
     #[test]
-    fn can_input_output_uint_graph_l1_glwe() {
-        input_output::<L1GlweCiphertext>();
+    fn can_input_output_generic_int_graph_l1_glwe() {
+        input_output::<L1GlweCiphertext, Unsigned>(1234);
+        input_output::<L1GlweCiphertext, Signed>(65432);
     }
 
     #[test]
     fn can_convert_ciphertexts() {
-        fn convert_test<T: CiphertextOps, U: CiphertextOps>() {
+        fn convert_test<T: CiphertextOps, U: CiphertextOps, V: Sign>(test_val: u64) {
             let graph = FheCircuitCtx::new();
             let enc = get_encryption_80();
             let (uproc, fc) = make_uproc_80();
             let sk = get_secret_keys_80();
 
-            let val = UInt::<16, T>::encrypt_secret(1234, &enc, &sk);
+            let val = GenericInt::<16, T, V>::encrypt_secret(test_val, &enc, &sk);
 
             let inputs = val.graph_inputs(&graph);
             let converted = inputs.convert::<U>(&graph);
@@ -468,30 +482,37 @@ mod tests {
                 .run_graph_blocking(&graph.circuit.borrow(), &fc);
 
             let actual = outputs.decrypt(&enc, &sk);
-            assert_eq!(actual, 1234);
+            assert_eq!(actual, test_val);
         }
 
-        convert_test::<L0LweCiphertext, L1GgswCiphertext>();
-        convert_test::<L0LweCiphertext, L1GlweCiphertext>();
-        convert_test::<L0LweCiphertext, L1LweCiphertext>();
-        convert_test::<L0LweCiphertext, L0LweCiphertext>();
+        convert_test::<L0LweCiphertext, L1GgswCiphertext, Unsigned>(1234);
+        convert_test::<L0LweCiphertext, L1GgswCiphertext, Signed>(65432);
+        convert_test::<L0LweCiphertext, L1GlweCiphertext, Unsigned>(1234);
+        convert_test::<L0LweCiphertext, L1GlweCiphertext, Signed>(65432);
+        convert_test::<L0LweCiphertext, L1LweCiphertext, Unsigned>(1234);
+        convert_test::<L0LweCiphertext, L1LweCiphertext, Signed>(65432);
+        convert_test::<L0LweCiphertext, L0LweCiphertext, Unsigned>(1234);
+        convert_test::<L0LweCiphertext, L0LweCiphertext, Signed>(65432);
 
         // GLEV ciphertexts are weird children, so give them a few cases.
-        convert_test::<L1GlevCiphertext, L1GgswCiphertext>();
-        convert_test::<L1GgswCiphertext, L1GlevCiphertext>();
-        convert_test::<L0LweCiphertext, L1GlevCiphertext>();
+        convert_test::<L1GlevCiphertext, L1GgswCiphertext, Unsigned>(1234);
+        convert_test::<L1GlevCiphertext, L1GgswCiphertext, Signed>(65432);
+        convert_test::<L1GgswCiphertext, L1GlevCiphertext, Unsigned>(1234);
+        convert_test::<L1GgswCiphertext, L1GlevCiphertext, Signed>(65432);
+        convert_test::<L0LweCiphertext, L1GlevCiphertext, Unsigned>(1234);
+        convert_test::<L0LweCiphertext, L1GlevCiphertext, Signed>(65432);
     }
 
     #[test]
     fn can_cmp() {
-        fn case<OutCt: Muxable>(gt: bool, eq: bool) {
+        fn case<OutCt: Muxable, U: Sign>(gt: bool, eq: bool, test_vals: (u64, u64)) {
             let enc = &get_encryption_80();
             let sk = get_secret_keys_80();
             let ctx = FheCircuitCtx::new();
             let (uproc, fc) = make_uproc_80();
 
-            let a = UInt::<16, L1GgswCiphertext>::encrypt_secret(43, enc, &sk);
-            let b = UInt::<16, L1GgswCiphertext>::encrypt_secret(42, enc, &sk);
+            let a = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.0, enc, &sk);
+            let b = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.1, enc, &sk);
 
             let a_input = a.graph_inputs(&ctx);
             let b_input = b.graph_inputs(&ctx);
@@ -516,29 +537,36 @@ mod tests {
             assert_eq!(expect_eq.decrypt(enc, &sk), eq);
         }
 
-        case::<L1GlweCiphertext>(false, false);
-        case::<L1GlweCiphertext>(false, true);
-        case::<L1GlweCiphertext>(true, false);
-        case::<L1GlweCiphertext>(true, true);
-
-        case::<L1GlevCiphertext>(false, false);
-        case::<L1GlevCiphertext>(false, true);
-        case::<L1GlevCiphertext>(true, false);
-        case::<L1GlevCiphertext>(true, true);
+        case::<L1GlweCiphertext, Unsigned>(false, false, (43, 42));
+        case::<L1GlweCiphertext, Signed>(false, false, (65501, 65500));
+        case::<L1GlweCiphertext, Signed>(false, false, (1, 65535));
+        case::<L1GlweCiphertext, Unsigned>(false, true, (43, 42));
+        case::<L1GlweCiphertext, Signed>(false, true, (65501, 65500));
+        case::<L1GlweCiphertext, Signed>(false, true, (1, 65535));
+        case::<L1GlweCiphertext, Unsigned>(true, false, (43, 42));
+        case::<L1GlweCiphertext, Signed>(true, false, (65501, 65500));
+        case::<L1GlweCiphertext, Signed>(true, false, (1, 65535));
+        case::<L1GlweCiphertext, Unsigned>(true, true, (43, 42));
+        case::<L1GlweCiphertext, Signed>(true, true, (65501, 65500));
+        case::<L1GlweCiphertext, Signed>(true, true, (1, 65535));
+
+        // Unsigned tests only for GLev
+        case::<L1GlevCiphertext, Unsigned>(false, false, (43, 42));
+        case::<L1GlevCiphertext, Unsigned>(false, true, (43, 42));
+        case::<L1GlevCiphertext, Unsigned>(true, false, (43, 42));
+        case::<L1GlevCiphertext, Unsigned>(true, true, (43, 42));
     }
 
     #[test]
     fn can_eq() {
-        fn case<OutCt: Muxable>(eq: bool) {
+        fn case<OutCt: Muxable, U: Sign>(eq: bool, test_vals: (u64, u64)) {
             let enc = &get_encryption_80();
             let sk = get_secret_keys_80();
             let ctx = FheCircuitCtx::new();
             let (uproc, fc) = make_uproc_80();
 
-            let (val_a, val_b) = if eq { (43, 43) } else { (43, 42) };
-
-            let a = UInt::<16, L1GgswCiphertext>::encrypt_secret(val_a, enc, &sk);
-            let b = UInt::<16, L1GgswCiphertext>::encrypt_secret(val_b, enc, &sk);
+            let a = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.0, enc, &sk);
+            let b = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.1, enc, &sk);
 
             let a_input = a.graph_inputs(&ctx);
             let b_input = b.graph_inputs(&ctx);
@@ -555,13 +583,17 @@ mod tests {
             assert_eq!(calculated_eq.decrypt(enc, &sk), eq);
         }
 
-        case::<L1GlweCiphertext>(false);
-        case::<L1GlweCiphertext>(true);
+        case::<L1GlweCiphertext, Unsigned>(false, (43, 42));
+        case::<L1GlweCiphertext, Signed>(false, (65501, 65500));
+        case::<L1GlweCiphertext, Unsigned>(true, (43, 43));
+        case::<L1GlweCiphertext, Signed>(true, (65501, 65501));
 
-        case::<L1GlevCiphertext>(false);
-        case::<L1GlevCiphertext>(true);
+        // Unsigned tests only for GLev
+        case::<L1GlevCiphertext, Unsigned>(false, (43, 42));
+        case::<L1GlevCiphertext, Unsigned>(true, (43, 43));
     }
 
+    // TODO for signed if interesting
     #[test]
     fn can_eq_size_mismatch() {
         fn case<const N: usize, const M: usize, OutCt: Muxable>(eq: bool) {
@@ -604,16 +636,14 @@ mod tests {
 
     #[test]
     fn can_neq() {
-        fn case<OutCt: Muxable>(neq: bool) {
+        fn case<OutCt: Muxable, U: Sign>(neq: bool, test_vals: (u64, u64)) {
             let enc = &get_encryption_80();
             let sk = get_secret_keys_80();
             let ctx = FheCircuitCtx::new();
             let (uproc, fc) = make_uproc_80();
 
-            let (val_a, val_b) = if neq { (43, 42) } else { (43, 43) };
-
-            let a = UInt::<16, L1GgswCiphertext>::encrypt_secret(val_a, enc, &sk);
-            let b = UInt::<16, L1GgswCiphertext>::encrypt_secret(val_b, enc, &sk);
+            let a = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.0, enc, &sk);
+            let b = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.1, enc, &sk);
 
             let a_input = a.graph_inputs(&ctx);
             let b_input = b.graph_inputs(&ctx);
@@ -630,12 +660,16 @@ mod tests {
             assert_eq!(calculated_neq.decrypt(enc, &sk), neq);
         }
 
-        case::<L1GlweCiphertext>(false);
-        case::<L1GlweCiphertext>(true);
-        case::<L1GlevCiphertext>(false);
-        case::<L1GlevCiphertext>(true);
+        case::<L1GlweCiphertext, Unsigned>(false, (43, 43));
+        case::<L1GlweCiphertext, Signed>(false, (65501, 65501));
+        case::<L1GlweCiphertext, Unsigned>(true, (43, 42));
+        case::<L1GlweCiphertext, Signed>(true, (65501, 65500));
+        // Unsigned tests only for GLev
+        case::<L1GlevCiphertext, Unsigned>(false, (43, 43));
+        case::<L1GlevCiphertext, Unsigned>(true, (43, 42));
     }
 
+    // TODO for signed if interesting
     #[test]
     fn can_neq_size_mismatch() {
         fn case<const N: usize, const M: usize, OutCt: Muxable>(neq: bool) {
@@ -676,6 +710,7 @@ mod tests {
         case::<16, 8, L1GlevCiphertext>(true);
     }
 
+    // TODO for signed if interesting
     #[test]
     fn can_cmp_size_mismatch() {
         fn case<const N: usize, const M: usize, OutCt: Muxable>(gt: bool, eq: bool) {
@@ -728,15 +763,15 @@ mod tests {
 
     #[test]
     fn can_cmp_trivial_nontrivial_ggsw() {
-        fn case<OutCt: Muxable>(gt: bool, eq: bool) {
+        fn case<OutCt: Muxable, U: Sign>(gt: bool, eq: bool, test_vals: (u64, u64)) {
             let enc = &get_encryption_80();
             let eval = &get_evaluation_80();
             let sk = get_secret_keys_80();
             let ctx = FheCircuitCtx::new();
             let (uproc, fc) = make_uproc_80();
 
-            let a = UInt::<16, L1GgswCiphertext>::trivial(43, enc, eval);
-            let b = UInt::<16, L1GgswCiphertext>::encrypt_secret(42, enc, &sk);
+            let a = GenericInt::<16, L1GgswCiphertext, U>::trivial(test_vals.0, enc, eval);
+            let b = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.1, enc, &sk);
 
             let a_input = a.graph_inputs(&ctx);
             let b_input = b.graph_inputs(&ctx);
@@ -761,17 +796,35 @@ mod tests {
             assert_eq!(expect_eq.decrypt(enc, &sk), eq);
         }
 
-        fn cases<OutCt: Muxable>() {
-            case::<OutCt>(false, false);
-            case::<OutCt>(false, true);
-            case::<OutCt>(true, false);
-            case::<OutCt>(true, true);
+        fn cases<OutCt: Muxable>(include_signed: bool) {
+            case::<OutCt, Unsigned>(false, false, (43, 42));
+            if include_signed {
+                case::<OutCt, Signed>(false, false, (65501, 65500));
+                case::<OutCt, Signed>(false, false, (1, 65535));
+            }
+            case::<OutCt, Unsigned>(false, true, (43, 42));
+            if include_signed {
+                case::<OutCt, Signed>(false, true, (65501, 65500));
+                case::<OutCt, Signed>(false, true, (1, 65535));
+            }
+            case::<OutCt, Unsigned>(true, false, (43, 42));
+            if include_signed {
+                case::<OutCt, Signed>(true, false, (65501, 65500));
+                case::<OutCt, Signed>(true, false, (1, 65535));
+            }
+            case::<OutCt, Unsigned>(true, true, (43, 42));
+            if include_signed {
+                case::<OutCt, Signed>(true, true, (65501, 65500));
+                case::<OutCt, Signed>(true, true, (1, 65535));
+            }
         }
 
-        cases::<L1GlweCiphertext>();
-        cases::<L1GlevCiphertext>();
+        cases::<L1GlweCiphertext>(true);
+        // Unsigned tests only for GLev
+        cases::<L1GlevCiphertext>(false);
     }
 
+    // TODO for signed if interesting
     #[test]
     fn can_select() {
         let enc = &get_encryption_80();
@@ -797,6 +850,7 @@ mod tests {
         assert_eq!(sel_true.decrypt(enc, &sk), 42);
     }
 
+    // TODO for signed if interesting
     #[test]
     fn can_select_plain() {
         let enc = &get_encryption_80();
@@ -827,14 +881,16 @@ mod tests {
 
     #[test]
     fn can_sub() {
-        fn case<OutCt: Muxable>() {
+        fn case<OutCt: Muxable, U: Sign>(test_vals: (u64, u64, u64)) {
             let enc = &get_encryption_80();
             let sk = get_secret_keys_80();
             let ctx = FheCircuitCtx::new();
             let (uproc, fc) = make_uproc_80();
 
-            let a = UInt::<16, L1GgswCiphertext>::encrypt_secret(42, enc, &sk).graph_inputs(&ctx);
-            let b = UInt::<16, L1GgswCiphertext>::encrypt_secret(16, enc, &sk).graph_inputs(&ctx);
+            let a = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.0, enc, &sk)
+                .graph_inputs(&ctx);
+            let b = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.1, enc, &sk)
+                .graph_inputs(&ctx);
 
             let c = a.sub::<OutCt>(&b, &ctx).collect_outputs(&ctx, enc);
 
@@ -843,33 +899,43 @@ mod tests {
                 .unwrap()
                 .run_graph_blocking(&ctx.circuit.borrow(), &fc);
 
-            assert_eq!(c.decrypt(enc, &sk), 26);
+            assert_eq!(c.decrypt(enc, &sk), test_vals.2);
         }
 
-        case::<L1GlweCiphertext>();
-        case::<L1GlevCiphertext>();
+        case::<L1GlweCiphertext, Unsigned>((42, 16, 26));
+        case::<L1GlweCiphertext, Signed>((65531, 65529, 2));
+        case::<L1GlweCiphertext, Signed>((65531, 2, 65529));
+        case::<L1GlweCiphertext, Signed>((65531, 65533, 65534));
+        case::<L1GlweCiphertext, Signed>((2, 65531, 7));
+        // Unsigned tests only for GLev
+        case::<L1GlevCiphertext, Unsigned>((42, 16, 26));
     }
 
     #[test]
-    fn trivial_uint_encryption() {
-        fn case<T: CiphertextOps>() {
+    fn trivial_generic_int_encryption() {
+        fn case<T: CiphertextOps, U: Sign>() {
             let enc = get_encryption_80();
             let eval = &get_evaluation_80();
             let sk = get_secret_keys_80();
 
             let expected = thread_rng().next_u64() % (0x1 << 32);
 
-            let val = UInt::<32, T>::trivial(expected, &enc, eval);
+            let val = GenericInt::<32, T, U>::trivial(expected, &enc, eval);
 
             assert_eq!(val.decrypt(&enc, &sk), expected);
         }
 
-        case::<L0LweCiphertext>();
-        case::<L1LweCiphertext>();
-        case::<L1GlweCiphertext>();
-        case::<L1GgswCiphertext>();
+        case::<L0LweCiphertext, Unsigned>();
+        case::<L0LweCiphertext, Signed>();
+        case::<L1LweCiphertext, Unsigned>();
+        case::<L1LweCiphertext, Signed>();
+        case::<L1GlweCiphertext, Unsigned>();
+        case::<L1GlweCiphertext, Signed>();
+        case::<L1GgswCiphertext, Unsigned>();
+        case::<L1GgswCiphertext, Signed>();
     }
 
+    // TODO for signed if interesting
     #[test]
     fn can_resize() {
         fn case<T: CiphertextOps>() {
@@ -912,14 +978,16 @@ mod tests {
 
     #[test]
     fn can_add() {
-        fn case<OutCt: Muxable>() {
+        fn case<OutCt: Muxable, U: Sign>(test_vals: (u64, u64, u64)) {
             let enc = get_encryption_80();
             let sk = get_secret_keys_80();
             let ctx = FheCircuitCtx::new();
             let (proc, fc) = make_uproc_80();
 
-            let a = UInt::<16, L1GgswCiphertext>::encrypt_secret(42, &enc, &sk).graph_inputs(&ctx);
-            let b = UInt::<16, L1GgswCiphertext>::encrypt_secret(16, &enc, &sk).graph_inputs(&ctx);
+            let a = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.0, &enc, &sk)
+                .graph_inputs(&ctx);
+            let b = GenericInt::<16, L1GgswCiphertext, U>::encrypt_secret(test_vals.1, &enc, &sk)
+                .graph_inputs(&ctx);
 
             let c = a.add::<OutCt>(&b, &ctx).collect_outputs(&ctx, &enc);
 
@@ -929,11 +997,15 @@ mod tests {
                 .unwrap()
                 .run_graph_blocking(&ctx.circuit.borrow(), &fc);
 
-            assert_eq!(c.decrypt(&enc, &sk), 58);
+            assert_eq!(c.decrypt(&enc, &sk), test_vals.2);
         }
 
-        case::<L1GlweCiphertext>();
-        case::<L1GlevCiphertext>();
+        case::<L1GlweCiphertext, Unsigned>((42, 16, 58));
+        case::<L1GlweCiphertext, Signed>((65530, 16, 10));
+        case::<L1GlweCiphertext, Signed>((65530, 65529, 65523));
+        case::<L1GlweCiphertext, Signed>((65528, 2, 65530));
+        // Unsigned tests only for GLev
+        case::<L1GlevCiphertext, Unsigned>((42, 16, 58));
     }
 
     #[test]
diff --git a/parasol_runtime/src/fluent/uint.rs b/parasol_runtime/src/fluent/uint.rs
index fd40a448..52ca7c6a 100644
--- a/parasol_runtime/src/fluent/uint.rs
+++ b/parasol_runtime/src/fluent/uint.rs
@@ -1,7 +1,7 @@
 use crate::{L1GgswCiphertext, circuits::mul::append_uint_multiply};
 
 use super::{
-    CiphertextOps, FheCircuitCtx, GeneratesCompareCircuit, Muxable, PackedGenericInt,
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
     bit::BitNode,
     generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
 };
@@ -12,7 +12,11 @@ use mux_circuits::comparisons::compare_or_maybe_equal;
 #[derive(Clone)]
 pub struct Unsigned;
 
-impl Sign for Unsigned {}
+impl Sign for Unsigned {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal(max_len, gt, eq)
+    }
+}
 
 /// Unsigned variant for [`GenericIntGraphNodes`]
 pub type UIntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Unsigned>;
@@ -42,12 +46,6 @@ impl<'a, const N: usize, T: CiphertextOps> UIntGraphNodes<'a, N, T> {
     }
 }
 
-impl<const N: usize> GeneratesCompareCircuit for UIntGraphNodes<'_, N, L1GgswCiphertext> {
-    fn gen_compare_circuit(&self, max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
-        compare_or_maybe_equal(max_len, gt, eq)
-    }
-}
-
 impl<'a, const N: usize> UIntGraphNodes<'a, N, L1GgswCiphertext> {
     /// Compute `self * other`.
     ///


Reviews:
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T16:59:57Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:00:23Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:05:20Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:05:55Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:06:07Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:06:51Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:08:24Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:08:49Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:09:06Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:09:36Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:10:26Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:10:32Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:11:30Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:13:19Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:13:44Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:14:19Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:15:07Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:15:17Z
user: login: rickwebiii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:02:16Z
user: login: rickwebiii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:03:01Z
user: login: rickwebiii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:04:23Z
user: login: rickwebiii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:06:30Z
user: login: rickwebiii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:08:49Z
user: login: rickwebiii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:09:47Z
user: login: rickwebiii
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:10:29Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:24:44Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:25:12Z
user: login: cedricjiang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T18:26:23Z

Review comments:
diff_hunk: @@ -12,6 +12,15 @@ use crate::{
     fluent::Muxable,
 };
 
+/// Compute the product of 2 N-bit unsigned values a and b.
+pub fn append_int_multiply<OutCt: Muxable>(
+    uop_graph: &mut FheCircuit,
+    a: &[NodeIndex],
+    b: &[NodeIndex],
+) -> (Vec<NodeIndex>, Vec<NodeIndex>) {
+    todo!("append_int_multiply ({:#?}, {:#?}, {:#?})", uop_graph, a, b);
path: parasol_runtime/src/circuits/mul.rs
user: login: cedricjiang
body: dummy...to be implemented...the message is to make compiler not to complain about the unused variables
created_at: 2025-04-14T16:59:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,8 +20,11 @@ use petgraph::stable_graph::NodeIndex;
 use serde::{Deserialize, Serialize};
 use sunscreen_tfhe::entities::Polynomial;
 
-/// Marker trait for distinguishing unsigned and signed integer types
-pub trait Sign {}
+/// Trait for distinguishing unsigned and signed integer types
+pub trait Sign {
+    /// Compare circuit generation function for this sign
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> MuxCircuit;
path: parasol_runtime/src/fluent/generic_int.rs
user: login: cedricjiang
body: moved the function to associate with this trait so the generic test works
created_at: 2025-04-14T17:00:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,8 +20,11 @@ use petgraph::stable_graph::NodeIndex;
 use serde::{Deserialize, Serialize};
 use sunscreen_tfhe::entities::Polynomial;
 
-/// Marker trait for distinguishing unsigned and signed integer types
-pub trait Sign {}
+/// Trait for distinguishing unsigned and signed integer types
+pub trait Sign {
+    /// Compare circuit generation function for this sign
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> MuxCircuit;
path: parasol_runtime/src/fluent/generic_int.rs
user: login: cedricjiang
body: may need to do this for functions that are individually implemented, that is `resize` and `mul`
created_at: 2025-04-14T17:05:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: pretty much a copy of unsigned :)
created_at: 2025-04-14T17:05:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: note the doc difference
created_at: 2025-04-14T17:06:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
+
+        let sign_bit = self.bits.last().unwrap();
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: copy sign bit for extension - is this correct? I assume ok since it's just an encrypted version of the sign bit
created_at: 2025-04-14T17:06:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
+
+        let sign_bit = self.bits.last().unwrap();
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: if not, I don't see a way to achieve signed extension
created_at: 2025-04-14T17:08:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
+
+        let sign_bit = self.bits.last().unwrap();
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: also for truncation, I kept the sign bit
created_at: 2025-04-14T17:08:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
+
+        let sign_bit = self.bits.last().unwrap();
+
+        let iter = self
+            .bits
+            .iter()
+            .copied()
+            .take(min_len)
+            .chain((0..extend).map(|_| sign_bit.to_owned()));
+
+        IntGraphNodes::from_bit_nodes(iter, &ctx.allocator)
+    }
+}
+
+impl<'a, const N: usize> IntGraphNodes<'a, N, L1GgswCiphertext> {
+    /// Compute `self * other`.
+    ///
+    /// # Remarks
+    /// Requires `self` and `other` to be [`L1GgswCiphertext`]s. Use [`Self::convert`] to
+    /// change to this type.
+    pub fn mul<OutCt: Muxable>(
+        &self,
+        other: &Self,
+        ctx: &'a FheCircuitCtx,
+    ) -> IntGraphNodes<'a, N, OutCt> {
+        let a = self.bits.iter().map(|x| x.node).collect::<Vec<_>>();
+
+        let b = other.bits.iter().map(|x| x.node).collect::<Vec<_>>();
+
+        let (lo, _hi) = append_int_multiply::<OutCt>(&mut ctx.circuit.borrow_mut(), &a, &b);
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: call the signed version (to be implemented)
created_at: 2025-04-14T17:09:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -360,6 +362,7 @@ impl Muxable for L1GlevCiphertext {
 #[cfg(test)]
 mod tests {
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: we can discuss how to deal with the tests here, including moving
created_at: 2025-04-14T17:09:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -389,30 +392,37 @@ mod tests {
 
     #[test]
     fn can_roundtrip_l0_lwe() {
-        roundtrip::<L0LweCiphertext>();
+        roundtrip::<L0LweCiphertext, Unsigned>();
+        roundtrip::<L0LweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_lwe() {
-        roundtrip::<L1LweCiphertext>();
+        roundtrip::<L1LweCiphertext, Unsigned>();
+        roundtrip::<L1LweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_glwe() {
-        roundtrip::<L1GlweCiphertext>();
+        roundtrip::<L1GlweCiphertext, Unsigned>();
+        roundtrip::<L1GlweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_ggsw() {
-        roundtrip::<L1GgswCiphertext>();
+        roundtrip::<L1GgswCiphertext, Unsigned>();
+        roundtrip::<L1GgswCiphertext, Signed>();
     }
 
-    fn input_output<T: CiphertextOps>() {
+    fn input_output<T: CiphertextOps, U: Sign>(test_val: u64) {
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: user to provide val so the signed version can use "negative" number
created_at: 2025-04-14T17:10:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -389,30 +392,37 @@ mod tests {
 
     #[test]
     fn can_roundtrip_l0_lwe() {
-        roundtrip::<L0LweCiphertext>();
+        roundtrip::<L0LweCiphertext, Unsigned>();
+        roundtrip::<L0LweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_lwe() {
-        roundtrip::<L1LweCiphertext>();
+        roundtrip::<L1LweCiphertext, Unsigned>();
+        roundtrip::<L1LweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_glwe() {
-        roundtrip::<L1GlweCiphertext>();
+        roundtrip::<L1GlweCiphertext, Unsigned>();
+        roundtrip::<L1GlweCiphertext, Signed>();
     }
 
     #[test]
     fn can_roundtrip_l1_ggsw() {
-        roundtrip::<L1GgswCiphertext>();
+        roundtrip::<L1GgswCiphertext, Unsigned>();
+        roundtrip::<L1GgswCiphertext, Signed>();
     }
 
-    fn input_output<T: CiphertextOps>() {
+    fn input_output<T: CiphertextOps, U: Sign>(test_val: u64) {
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: same for a few below
created_at: 2025-04-14T17:10:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -516,29 +537,43 @@ mod tests {
             assert_eq!(expect_eq.decrypt(enc, &sk), eq);
         }
 
-        case::<L1GlweCiphertext>(false, false);
-        case::<L1GlweCiphertext>(false, true);
-        case::<L1GlweCiphertext>(true, false);
-        case::<L1GlweCiphertext>(true, true);
-
-        case::<L1GlevCiphertext>(false, false);
-        case::<L1GlevCiphertext>(false, true);
-        case::<L1GlevCiphertext>(true, false);
-        case::<L1GlevCiphertext>(true, true);
+        case::<L1GlweCiphertext, Unsigned>(false, false, (43, 42));
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: too many lines, probably should use some for loop
created_at: 2025-04-14T17:11:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -630,12 +668,17 @@ mod tests {
             assert_eq!(calculated_neq.decrypt(enc, &sk), neq);
         }
 
-        case::<L1GlweCiphertext>(false);
-        case::<L1GlweCiphertext>(true);
-        case::<L1GlevCiphertext>(false);
-        case::<L1GlevCiphertext>(true);
+        case::<L1GlweCiphertext, Unsigned>(false, (43, 43));
+        case::<L1GlweCiphertext, Signed>(false, (65501, 65501));
+        case::<L1GlweCiphertext, Unsigned>(true, (43, 42));
+        case::<L1GlweCiphertext, Signed>(true, (65501, 65500));
+        case::<L1GlevCiphertext, Unsigned>(false, (43, 43));
+        case::<L1GlevCiphertext, Signed>(false, (65501, 65501));
+        case::<L1GlevCiphertext, Unsigned>(true, (43, 42));
+        case::<L1GlevCiphertext, Signed>(true, (65501, 65500));
     }
 
+    // TODO for signed
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: I don't know how to do these correctly yet for signed. My understanding is the underlying code only deal with matched size, so before going there some code will take care of the unmatched size (essentially "resize"). I need to think a bit more about how to make that code do the correct resizing. May or may not do this in this PR
created_at: 2025-04-14T17:13:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -630,12 +668,17 @@ mod tests {
             assert_eq!(calculated_neq.decrypt(enc, &sk), neq);
         }
 
-        case::<L1GlweCiphertext>(false);
-        case::<L1GlweCiphertext>(true);
-        case::<L1GlevCiphertext>(false);
-        case::<L1GlevCiphertext>(true);
+        case::<L1GlweCiphertext, Unsigned>(false, (43, 43));
+        case::<L1GlweCiphertext, Signed>(false, (65501, 65501));
+        case::<L1GlweCiphertext, Unsigned>(true, (43, 42));
+        case::<L1GlweCiphertext, Signed>(true, (65501, 65500));
+        case::<L1GlevCiphertext, Unsigned>(false, (43, 43));
+        case::<L1GlevCiphertext, Signed>(false, (65501, 65501));
+        case::<L1GlevCiphertext, Unsigned>(true, (43, 42));
+        case::<L1GlevCiphertext, Signed>(true, (65501, 65500));
     }
 
+    // TODO for signed
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: same for other "size mismatch" tests
created_at: 2025-04-14T17:13:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -762,16 +806,25 @@ mod tests {
         }
 
         fn cases<OutCt: Muxable>() {
-            case::<OutCt>(false, false);
-            case::<OutCt>(false, true);
-            case::<OutCt>(true, false);
-            case::<OutCt>(true, true);
+            case::<OutCt, Unsigned>(false, false, (43, 42));
+            case::<OutCt, Signed>(false, false, (65501, 65500));
+            // case::<OutCt, Signed>(false, false, (1, 65535));
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: this one is commented out because it fails...need to understand why
created_at: 2025-04-14T17:14:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -762,16 +806,25 @@ mod tests {
         }
 
         fn cases<OutCt: Muxable>() {
-            case::<OutCt>(false, false);
-            case::<OutCt>(false, true);
-            case::<OutCt>(true, false);
-            case::<OutCt>(true, true);
+            case::<OutCt, Unsigned>(false, false, (43, 42));
+            case::<OutCt, Signed>(false, false, (65501, 65500));
+            // case::<OutCt, Signed>(false, false, (1, 65535));
+            case::<OutCt, Unsigned>(false, true, (43, 42));
+            case::<OutCt, Signed>(false, true, (65501, 65500));
+            // case::<OutCt, Signed>(false, true, (1, 65535));
+            case::<OutCt, Unsigned>(true, false, (43, 42));
+            case::<OutCt, Signed>(true, false, (65501, 65500));
+            // case::<OutCt, Signed>(true, false, (1, 65535));
+            case::<OutCt, Unsigned>(true, true, (43, 42));
+            case::<OutCt, Signed>(true, true, (65501, 65500));
+            // case::<OutCt, Signed>(true, true, (1, 65535));
         }
 
         cases::<L1GlweCiphertext>();
         cases::<L1GlevCiphertext>();
     }
 
+    // TODO for signed
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: this test function has only one test cases so there's no inner "case" function to generalize, but easy to refactor. I will likely include include it in this PR
created_at: 2025-04-14T17:15:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -762,16 +806,25 @@ mod tests {
         }
 
         fn cases<OutCt: Muxable>() {
-            case::<OutCt>(false, false);
-            case::<OutCt>(false, true);
-            case::<OutCt>(true, false);
-            case::<OutCt>(true, true);
+            case::<OutCt, Unsigned>(false, false, (43, 42));
+            case::<OutCt, Signed>(false, false, (65501, 65500));
+            // case::<OutCt, Signed>(false, false, (1, 65535));
+            case::<OutCt, Unsigned>(false, true, (43, 42));
+            case::<OutCt, Signed>(false, true, (65501, 65500));
+            // case::<OutCt, Signed>(false, true, (1, 65535));
+            case::<OutCt, Unsigned>(true, false, (43, 42));
+            case::<OutCt, Signed>(true, false, (65501, 65500));
+            // case::<OutCt, Signed>(true, false, (1, 65535));
+            case::<OutCt, Unsigned>(true, true, (43, 42));
+            case::<OutCt, Signed>(true, true, (65501, 65500));
+            // case::<OutCt, Signed>(true, true, (1, 65535));
         }
 
         cases::<L1GlweCiphertext>();
         cases::<L1GlevCiphertext>();
     }
 
+    // TODO for signed
path: parasol_runtime/src/fluent/mod.rs
user: login: cedricjiang
body: same for the "plain" version below
created_at: 2025-04-14T17:15:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
+
+        let sign_bit = self.bits.last().unwrap();
path: parasol_runtime/src/fluent/int.rs
user: login: rickwebiii
body: Yeah, you want to do a signed extension. 
created_at: 2025-04-15T18:02:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
+
+        let sign_bit = self.bits.last().unwrap();
path: parasol_runtime/src/fluent/int.rs
user: login: rickwebiii
body: Truncation is just that. If you lose the sign bit and now get a positive number, then so be it.
created_at: 2025-04-15T18:03:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
path: parasol_runtime/src/fluent/int.rs
user: login: rickwebiii
body: I think this should not be -1.
created_at: 2025-04-15T18:04:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
path: parasol_runtime/src/fluent/int.rs
user: login: rickwebiii
body: I think there should not be a +1 here.
created_at: 2025-04-15T18:06:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -516,29 +537,43 @@ mod tests {
             assert_eq!(expect_eq.decrypt(enc, &sk), eq);
         }
 
-        case::<L1GlweCiphertext>(false, false);
-        case::<L1GlweCiphertext>(false, true);
-        case::<L1GlweCiphertext>(true, false);
-        case::<L1GlweCiphertext>(true, true);
-
-        case::<L1GlevCiphertext>(false, false);
-        case::<L1GlevCiphertext>(false, true);
-        case::<L1GlevCiphertext>(true, false);
-        case::<L1GlevCiphertext>(true, true);
+        case::<L1GlweCiphertext, Unsigned>(false, false, (43, 42));
+        case::<L1GlweCiphertext, Signed>(false, false, (65501, 65500));
+        case::<L1GlweCiphertext, Signed>(false, false, (1, 65535));
+        case::<L1GlweCiphertext, Unsigned>(false, true, (43, 42));
+        case::<L1GlweCiphertext, Signed>(false, true, (65501, 65500));
+        case::<L1GlweCiphertext, Signed>(false, true, (1, 65535));
+        case::<L1GlweCiphertext, Unsigned>(true, false, (43, 42));
+        case::<L1GlweCiphertext, Signed>(true, false, (65501, 65500));
+        case::<L1GlweCiphertext, Signed>(true, false, (1, 65535));
+        case::<L1GlweCiphertext, Unsigned>(true, true, (43, 42));
+        case::<L1GlweCiphertext, Signed>(true, true, (65501, 65500));
+        case::<L1GlweCiphertext, Signed>(true, true, (1, 65535));
+
+        case::<L1GlevCiphertext, Unsigned>(false, false, (43, 42));
+        // case::<L1GlevCiphertext, Signed>(false, false, (65501, 65500));
path: parasol_runtime/src/fluent/mod.rs
user: login: rickwebiii
body: Remove commented lines
created_at: 2025-04-15T18:08:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -630,12 +668,17 @@ mod tests {
             assert_eq!(calculated_neq.decrypt(enc, &sk), neq);
         }
 
-        case::<L1GlweCiphertext>(false);
-        case::<L1GlweCiphertext>(true);
-        case::<L1GlevCiphertext>(false);
-        case::<L1GlevCiphertext>(true);
+        case::<L1GlweCiphertext, Unsigned>(false, (43, 43));
+        case::<L1GlweCiphertext, Signed>(false, (65501, 65501));
+        case::<L1GlweCiphertext, Unsigned>(true, (43, 42));
+        case::<L1GlweCiphertext, Signed>(true, (65501, 65500));
+        case::<L1GlevCiphertext, Unsigned>(false, (43, 43));
+        case::<L1GlevCiphertext, Signed>(false, (65501, 65501));
+        case::<L1GlevCiphertext, Unsigned>(true, (43, 42));
+        case::<L1GlevCiphertext, Signed>(true, (65501, 65500));
     }
 
+    // TODO for signed
path: parasol_runtime/src/fluent/mod.rs
user: login: rickwebiii
body: Mismatched length equality checking is not super high-pri
created_at: 2025-04-15T18:09:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: if we preserve the sign bit then we +1 here, the reason for that is the min_len has -1 so the sign bit is not included anyway in either truncation or extension, added a comment.
created_at: 2025-04-15T18:24:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: see above, this helps make the code consistent in truncation (although we don't necessarily care about that...)
created_at: 2025-04-15T18:25:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,193 @@
+use crate::{L1GgswCiphertext, circuits::mul::append_int_multiply};
+
+use super::{
+    CiphertextOps, FheCircuitCtx, Muxable, PackedGenericInt,
+    generic_int::{GenericInt, GenericIntGraphNodes, PackedGenericIntGraphNode, Sign},
+};
+
+use mux_circuits::comparisons::compare_or_maybe_equal_signed;
+
+/// Marker struct
+#[derive(Clone)]
+pub struct Signed;
+
+impl Sign for Signed {
+    fn gen_compare_circuit(max_len: usize, gt: bool, eq: bool) -> mux_circuits::MuxCircuit {
+        compare_or_maybe_equal_signed(max_len, gt, eq)
+    }
+}
+
+/// Signed variant for [`GenericIntGraphNodes`]
+pub type IntGraphNodes<'a, const N: usize, T> = GenericIntGraphNodes<'a, N, T, Signed>;
+
+impl<'a, const N: usize, T: CiphertextOps> IntGraphNodes<'a, N, T> {
+    /// Convert this `N`-bit integer to an `M`-bit integer of the same ciphertext type.
+    ///
+    /// # Remarks
+    /// If M > N, this will sign extend the integer.
+    /// If M < N, this will truncate the high-order bits with sign bit preserved.
+    /// If M == N, why did you call this? In any case, the returned nodes will equal the input nodes.
+    ///
+    /// This operation is "free" in that it adds no computation to the graph.
+    pub fn resize<const M: usize>(&self, ctx: &'a FheCircuitCtx) -> IntGraphNodes<'a, M, T> {
+        let extend = 1 + if M > N { M - N } else { 0 };
+
+        let min_len = M.min(N) - 1;
path: parasol_runtime/src/fluent/int.rs
user: login: cedricjiang
body: by doing so (preserving the sign bit in truncation) the code is more consistent as you don't need to handle truncation and extension differently
created_at: 2025-04-15T18:26:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: cedricjiang
created_at: 2025-04-14T17:25:20Z
body: I am bothered by that `cargo test` succeeded locally (however when I `cargo clean` and then `cargo test` it failed)
author_association: CONTRIBUTOR
user: login: cedricjiang
created_at: 2025-04-14T17:28:58Z
body: Hmm...tests seem flaky now
author_association: CONTRIBUTOR