Repository: gleam-lang/gleam (rust)

Issue:
Permit maths expressions in bit array size patterns
<!--
Please include the following:
- Steps and code to reproduce the problem
- Versions for Gleam, OS, the runtime and any other software used
- Logs from running Gleam with the `GLEAM_LOG` environment variable set to `trace`
-->

OS: Linux
Gleam: 1.10.0

When using a guard expression inside `size` for bit array parsing, it complains. This should be a self-contined example showing the issue.

```gleam
pub fn bmc_packet(data: BitArray) {
  case data {
    <<
      packet_size:16,
      packet_data:bytes-size(packet_size-12),
    >> -> {
        echo packet_size
        echo packet_data
    }
    _ -> {
      echo "blah"
    }
  }
}
```

The error provided by the LSP/Linter: 
```
Syntax error
I was not expecting this.
Found `-`, expected one of: 
- `)`
```

1.11.0-rc doesn't work either:

```
❯ ~/Downloads/gleam-v1.11.0-rc1-x86_64-unknown-linux-musl/gleam run
error: Syntax error
   ┌─ /var/home/yulian/workspace/code/bmc/server-gleam/src/packets.gleam:51:41
   │
51 │       packet_data:bytes-size(packet_size-12),
   │                                         ^ I was not expecting this

Found `-`, expected one of: 
- `)`
❯ ~/Downloads/gleam-v1.11.0-rc1-x86_64-unknown-linux-musl/gleam --version
gleam 1.11.0-rc1
```

According to https://www.erlang.org/doc/system/bit_syntax.html#binding-and-using-a-size-variable

Ths should be possible as a guard expression as of OTP 23. Their example:

```erlang
bar(<<Sz:8,Payload:((Sz-1)*8)/binary,Rest/binary>>) ->
   {Payload,Rest}.

Here Sz is the combined size of the header and the payload, so we will need to subtract one byte to get the size of the payload.
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1ba884d200..2003ae325a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -200,6 +200,15 @@
   construction or pattern matching on bit arrays.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- Calculations are now allowed in the size options of bit array patterns. For
+  example, the following code is now valid:
+
+  ```gleam
+  let assert <<size, data:bytes-size(size / 8 - 1)>> = some_bit_array
+  ```
+
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ### Build tool
 
 - `gleam update`, `gleam deps update`, and `gleam deps download` will now print
diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs
index a8442aed4de..f4b0cfeb665 100644
--- a/compiler-core/src/ast.rs
+++ b/compiler-core/src/ast.rs
@@ -23,6 +23,7 @@ use crate::type_::{
     self, Deprecation, HasType, ModuleValueConstructor, PatternConstructor, Type, TypedCallArg,
     ValueConstructor, nil,
 };
+use num_traits::Zero;
 use std::collections::HashSet;
 use std::sync::Arc;
 
@@ -2015,15 +2016,10 @@ pub enum Pattern<Type> {
         origin: VariableOrigin,
     },
 
-    /// A reference to a variable in a bit array. This is always a variable
-    /// being used rather than a new variable being assigned.
-    /// e.g. `assert <<y:size(somevar)>> = x`
-    VarUsage {
-        location: SrcSpan,
-        name: EcoString,
-        constructor: Option<ValueConstructor>,
-        type_: Type,
-    },
+    /// The specified size of a bit array. This can either be a literal integer,
+    /// a reference to a variable, or a maths expression.
+    /// e.g. `let assert <<y:size(somevar)>> = x`
+    BitArraySize(BitArraySize<Type>),
 
     /// A name given to a sub-pattern using the `as` keyword.
     /// e.g. `assert #(1, [_, _] as the_list) = x`
@@ -2089,6 +2085,84 @@ pub enum Pattern<Type> {
     },
 }
 
+pub type TypedBitArraySize = BitArraySize<Arc<Type>>;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum BitArraySize<Type> {
+    Int {
+        location: SrcSpan,
+        value: EcoString,
+        int_value: BigInt,
+    },
+
+    Variable {
+        location: SrcSpan,
+        name: EcoString,
+        constructor: Option<Box<ValueConstructor>>,
+        type_: Type,
+    },
+
+    BinaryOperator {
+        location: SrcSpan,
+        operator: IntOperator,
+        left: Box<Self>,
+        right: Box<Self>,
+    },
+
+    Block {
+        location: SrcSpan,
+        inner: Box<Self>,
+    },
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum IntOperator {
+    Add,
+    Subtract,
+    Multiply,
+    Divide,
+    Remainder,
+}
+
+impl IntOperator {
+    pub fn precedence(&self) -> u8 {
+        match self {
+            Self::Add | Self::Subtract => 7,
+
+            Self::Multiply | Self::Divide | Self::Remainder => 8,
+        }
+    }
+
+    pub fn to_bin_op(&self) -> BinOp {
+        match self {
+            IntOperator::Add => BinOp::AddInt,
+            IntOperator::Subtract => BinOp::SubInt,
+            IntOperator::Multiply => BinOp::MultInt,
+            IntOperator::Divide => BinOp::DivInt,
+            IntOperator::Remainder => BinOp::RemainderInt,
+        }
+    }
+}
+
+impl<T> BitArraySize<T> {
+    pub fn location(&self) -> SrcSpan {
+        match self {
+            BitArraySize::Int { location, .. }
+            | BitArraySize::Variable { location, .. }
+            | BitArraySize::BinaryOperator { location, .. }
+            | BitArraySize::Block { location, .. } => *location,
+        }
+    }
+
+    pub fn non_zero_compile_time_number(&self) -> bool {
+        match self {
+            BitArraySize::Int { int_value, .. } => !int_value.is_zero(),
+            BitArraySize::Block { inner, .. } => inner.non_zero_compile_time_number(),
+            BitArraySize::Variable { .. } | BitArraySize::BinaryOperator { .. } => false,
+        }
+    }
+}
+
 impl Default for Inferred<()> {
     fn default() -> Self {
         Self::Unknown
@@ -2131,7 +2205,6 @@ impl<A> Pattern<A> {
             } => SrcSpan::new(pattern.location().start, location.end),
             Pattern::Int { location, .. }
             | Pattern::Variable { location, .. }
-            | Pattern::VarUsage { location, .. }
             | Pattern::List { location, .. }
             | Pattern::Float { location, .. }
             | Pattern::Discard { location, .. }
@@ -2141,6 +2214,7 @@ impl<A> Pattern<A> {
             | Pattern::StringPrefix { location, .. }
             | Pattern::BitArray { location, .. }
             | Pattern::Invalid { location, .. } => *location,
+            Pattern::BitArraySize(size) => size.location(),
         }
     }
 
@@ -2173,7 +2247,7 @@ impl TypedPattern {
             | Pattern::Float { .. }
             | Pattern::String { .. }
             | Pattern::Variable { .. }
-            | Pattern::VarUsage { .. }
+            | Pattern::BitArraySize { .. }
             | Pattern::Assign { .. }
             | Pattern::Discard { .. }
             | Pattern::List { .. }
@@ -2192,7 +2266,7 @@ impl TypedPattern {
             | Pattern::Float { .. }
             | Pattern::String { .. }
             | Pattern::Variable { .. }
-            | Pattern::VarUsage { .. }
+            | Pattern::BitArraySize { .. }
             | Pattern::Assign { .. }
             | Pattern::Discard { .. }
             | Pattern::List { .. }
@@ -2215,12 +2289,14 @@ impl TypedPattern {
 
             Pattern::Variable { type_, .. }
             | Pattern::List { type_, .. }
-            | Pattern::VarUsage { type_, .. }
             | Pattern::Constructor { type_, .. }
             | Pattern::Invalid { type_, .. } => type_.clone(),
 
             Pattern::Assign { pattern, .. } => pattern.type_(),
 
+            // Bit array sizes should always be integers
+            Pattern::BitArraySize(_) => type_::int(),
+
             Pattern::Discard { type_, .. } => type_.clone(),
 
             Pattern::Tuple { elements, .. } => {
@@ -2246,7 +2322,7 @@ impl TypedPattern {
             | Pattern::Float { .. }
             | Pattern::String { .. }
             | Pattern::Variable { .. }
-            | Pattern::VarUsage { .. }
+            | Pattern::BitArraySize { .. }
             | Pattern::Assign { .. }
             | Pattern::Discard { .. }
             | Pattern::StringPrefix { .. }
@@ -2329,7 +2405,7 @@ impl TypedPattern {
             Pattern::Int { .. }
             | Pattern::Float { .. }
             | Pattern::String { .. }
-            | Pattern::VarUsage { .. }
+            | Pattern::BitArraySize { .. }
             | Pattern::List { .. }
             | Pattern::Constructor { .. }
             | Pattern::BitArray { .. }
@@ -2353,7 +2429,7 @@ impl TypedPattern {
             | Pattern::Invalid { .. } => {}
 
             Pattern::Variable { name, .. } => variables.push(name.clone()),
-            Pattern::VarUsage { .. } => {}
+            Pattern::BitArraySize { .. } => {}
             Pattern::Assign { name, pattern, .. } => {
                 variables.push(name.clone());
                 pattern.collect_bound_variables(variables);
@@ -2501,7 +2577,7 @@ impl<Type> BitArraySegment<Pattern<Type>, Type> {
             | Pattern::Float { .. }
             | Pattern::String { .. }
             | Pattern::Variable { .. }
-            | Pattern::VarUsage { .. }
+            | Pattern::BitArraySize { .. }
             | Pattern::Discard { .. }
             | Pattern::List { .. }
             | Pattern::Constructor { .. }
diff --git a/compiler-core/src/ast/visit.rs b/compiler-core/src/ast/visit.rs
index 17a55b25516..749e74252a3 100644
--- a/compiler-core/src/ast/visit.rs
+++ b/compiler-core/src/ast/visit.rs
@@ -40,6 +40,7 @@
 
 use crate::{
     analyse::Inferred,
+    ast::{BitArraySize, TypedBitArraySize},
     exhaustiveness::CompiledCase,
     type_::{
         ModuleValueConstructor, PatternConstructor, TypedCallArg, ValueConstructor,
@@ -435,14 +436,22 @@ pub trait Visit<'ast> {
         visit_typed_pattern_variable(self, location, name, type_, origin);
     }
 
-    fn visit_typed_pattern_var_usage(
+    fn visit_typed_pattern_bit_array_size(&mut self, size: &'ast TypedBitArraySize) {
+        visit_typed_pattern_bit_array_size(self, size);
+    }
+
+    fn visit_typed_bit_array_size_int(&mut self, location: &'ast SrcSpan, value: &'ast EcoString) {
+        visit_typed_bit_array_size_int(self, location, value)
+    }
+
+    fn visit_typed_bit_array_size_variable(
         &mut self,
         location: &'ast SrcSpan,
         name: &'ast EcoString,
-        constructor: &'ast Option<ValueConstructor>,
+        constructor: &'ast Option<Box<ValueConstructor>>,
         type_: &'ast Arc<Type>,
     ) {
-        visit_typed_pattern_var_usage(self, location, name, constructor, type_);
+        visit_typed_bit_array_size_variable(self, location, name, constructor, type_)
     }
 
     fn visit_typed_pattern_assign(
@@ -1493,12 +1502,7 @@ where
             type_,
             origin,
         } => v.visit_typed_pattern_variable(location, name, type_, origin),
-        Pattern::VarUsage {
-            location,
-            name,
-            constructor,
-            type_,
-        } => v.visit_typed_pattern_var_usage(location, name, constructor, type_),
+        Pattern::BitArraySize(size) => v.visit_typed_pattern_bit_array_size(size),
         Pattern::Assign {
             location,
             name,
@@ -1586,12 +1590,45 @@ pub fn visit_typed_pattern_variable<'a, V>(
 {
 }
 
-pub fn visit_typed_pattern_var_usage<'a, V>(
+pub fn visit_typed_pattern_bit_array_size<'a, V>(v: &mut V, size: &'a TypedBitArraySize)
+where
+    V: Visit<'a> + ?Sized,
+{
+    match size {
+        BitArraySize::Int {
+            location,
+            value,
+            int_value: _,
+        } => v.visit_typed_bit_array_size_int(location, value),
+        BitArraySize::Variable {
+            location,
+            name,
+            constructor,
+            type_,
+        } => v.visit_typed_bit_array_size_variable(location, name, constructor, type_),
+        BitArraySize::BinaryOperator { left, right, .. } => {
+            v.visit_typed_pattern_bit_array_size(left);
+            v.visit_typed_pattern_bit_array_size(right);
+        }
+        BitArraySize::Block { inner, .. } => v.visit_typed_pattern_bit_array_size(inner),
+    }
+}
+
+pub fn visit_typed_bit_array_size_int<'a, V>(
+    _v: &mut V,
+    _location: &'a SrcSpan,
+    _value: &'a EcoString,
+) where
+    V: Visit<'a> + ?Sized,
+{
+}
+
+pub fn visit_typed_bit_array_size_variable<'a, V>(
     _v: &mut V,
     _location: &'a SrcSpan,
     _name: &'a EcoString,
-    _constructor: &'a Option<ValueConstructor>,
-    _type: &'a Arc<Type>,
+    _constructor: &'a Option<Box<ValueConstructor>>,
+    _type_: &'a Arc<Type>,
 ) where
     V: Visit<'a> + ?Sized,
 {
diff --git a/compiler-core/src/ast_folder.rs b/compiler-core/src/ast_folder.rs
index 012b41b319d..b35a600606d 100644
--- a/compiler-core/src/ast_folder.rs
+++ b/compiler-core/src/ast_folder.rs
@@ -5,10 +5,10 @@ use vec1::Vec1;
 use crate::{
     analyse::Inferred,
     ast::{
-        Assert, AssignName, Assignment, BinOp, CallArg, Constant, Definition, FunctionLiteralKind,
-        Pattern, RecordBeingUpdated, SrcSpan, Statement, TargetedDefinition, TodoKind, TypeAst,
-        TypeAstConstructor, TypeAstFn, TypeAstHole, TypeAstTuple, TypeAstVar, UntypedArg,
-        UntypedAssert, UntypedAssignment, UntypedClause, UntypedConstant,
+        Assert, AssignName, Assignment, BinOp, BitArraySize, CallArg, Constant, Definition,
+        FunctionLiteralKind, Pattern, RecordBeingUpdated, SrcSpan, Statement, TargetedDefinition,
+        TodoKind, TypeAst, TypeAstConstructor, TypeAstFn, TypeAstHole, TypeAstTuple, TypeAstVar,
+        UntypedArg, UntypedAssert, UntypedAssignment, UntypedClause, UntypedConstant,
         UntypedConstantBitArraySegment, UntypedCustomType, UntypedDefinition, UntypedExpr,
         UntypedExprBitArraySegment, UntypedFunction, UntypedImport, UntypedModule,
         UntypedModuleConstant, UntypedPattern, UntypedPatternBitArraySegment,
@@ -1193,12 +1193,7 @@ pub trait PatternFolder {
                 origin,
             } => self.fold_pattern_var(location, name, origin),
 
-            Pattern::VarUsage {
-                location,
-                name,
-                constructor: _,
-                type_: (),
-            } => self.fold_pattern_var_usage(location, name),
+            Pattern::BitArraySize(size) => self.fold_pattern_bit_array_size(size),
 
             Pattern::Assign {
                 name,
@@ -1298,8 +1293,57 @@ pub trait PatternFolder {
         }
     }
 
-    fn fold_pattern_var_usage(&mut self, location: SrcSpan, name: EcoString) -> UntypedPattern {
-        Pattern::VarUsage {
+    fn fold_pattern_bit_array_size(&mut self, size: BitArraySize<()>) -> UntypedPattern {
+        Pattern::BitArraySize(self.fold_bit_array_size(size))
+    }
+
+    fn fold_bit_array_size(&mut self, size: BitArraySize<()>) -> BitArraySize<()> {
+        match size {
+            BitArraySize::Int {
+                location,
+                value,
+                int_value,
+            } => self.fold_bit_array_size_int(location, value, int_value),
+            BitArraySize::Variable { location, name, .. } => {
+                self.fold_bit_array_size_variable(location, name)
+            }
+            BitArraySize::BinaryOperator {
+                location,
+                operator,
+                left,
+                right,
+            } => BitArraySize::BinaryOperator {
+                location,
+                operator,
+                left: Box::new(self.fold_bit_array_size(*left)),
+                right: Box::new(self.fold_bit_array_size(*right)),
+            },
+            BitArraySize::Block { location, inner } => BitArraySize::Block {
+                location,
+                inner: Box::new(self.fold_bit_array_size(*inner)),
+            },
+        }
+    }
+
+    fn fold_bit_array_size_int(
+        &mut self,
+        location: SrcSpan,
+        value: EcoString,
+        int_value: BigInt,
+    ) -> BitArraySize<()> {
+        BitArraySize::Int {
+            location,
+            value,
+            int_value,
+        }
+    }
+
+    fn fold_bit_array_size_variable(
+        &mut self,
+        location: SrcSpan,
+        name: EcoString,
+    ) -> BitArraySize<()> {
+        BitArraySize::Variable {
             location,
             name,
             constructor: None,
@@ -1413,7 +1457,7 @@ pub trait PatternFolder {
             | Pattern::Float { .. }
             | Pattern::String { .. }
             | Pattern::Discard { .. }
-            | Pattern::VarUsage { .. }
+            | Pattern::BitArraySize { .. }
             | Pattern::StringPrefix { .. }
             | Pattern::Invalid { .. } => pattern,
 
diff --git a/compiler-core/src/bit_array.rs b/compiler-core/src/bit_array.rs
index 027a52539a9..fa62afe9b80 100644
--- a/compiler-core/src/bit_array.rs
+++ b/compiler-core/src/bit_array.rs
@@ -344,10 +344,12 @@ pub trait GetLiteralValue {
 
 impl GetLiteralValue for ast::TypedPattern {
     fn as_int_literal(&self) -> Option<BigInt> {
-        if let ast::Pattern::Int { int_value, .. } = self {
-            Some(int_value.clone())
-        } else {
-            None
+        match self {
+            ast::Pattern::Int { int_value, .. }
+            | ast::Pattern::BitArraySize(ast::BitArraySize::Int { int_value, .. }) => {
+                Some(int_value.clone())
+            }
+            _ => None,
         }
     }
 }
diff --git a/compiler-core/src/call_graph.rs b/compiler-core/src/call_graph.rs
index 00746fcba5b..d2b9bce6366 100644
--- a/compiler-core/src/call_graph.rs
+++ b/compiler-core/src/call_graph.rs
@@ -7,9 +7,9 @@ mod into_dependency_order_tests;
 use crate::{
     Result,
     ast::{
-        AssignName, AssignmentKind, BitArrayOption, ClauseGuard, Constant, Pattern, SrcSpan,
-        Statement, UntypedClauseGuard, UntypedExpr, UntypedFunction, UntypedModuleConstant,
-        UntypedPattern, UntypedStatement,
+        AssignName, AssignmentKind, BitArrayOption, BitArraySize, ClauseGuard, Constant, Pattern,
+        SrcSpan, Statement, UntypedClauseGuard, UntypedExpr, UntypedFunction,
+        UntypedModuleConstant, UntypedPattern, UntypedStatement,
     },
     type_::Error,
 };
@@ -354,8 +354,8 @@ impl<'a> CallGraphBuilder<'a> {
                 }
             }
 
-            Pattern::VarUsage { name, .. } => {
-                self.referenced(name);
+            Pattern::BitArraySize(size) => {
+                self.bit_array_size(size);
             }
 
             Pattern::Assign { name, pattern, .. } => {
@@ -380,6 +380,18 @@ impl<'a> CallGraphBuilder<'a> {
         }
     }
 
+    fn bit_array_size(&mut self, size: &'a BitArraySize<()>) {
+        match size {
+            BitArraySize::Int { .. } => {}
+            BitArraySize::Variable { name, .. } => self.referenced(name),
+            BitArraySize::BinaryOperator { left, right, .. } => {
+                self.bit_array_size(left);
+                self.bit_array_size(right);
+            }
+            BitArraySize::Block { inner, .. } => self.bit_array_size(inner),
+        }
+    }
+
     fn define(&mut self, name: &'a str) {
         _ = self.names.insert(name, None);
     }
diff --git a/compiler-core/src/erlang/pattern.rs b/compiler-core/src/erlang/pattern.rs
index a7d075ef573..a12288e0f69 100644
--- a/compiler-core/src/erlang/pattern.rs
+++ b/compiler-core/src/erlang/pattern.rs
@@ -37,20 +37,12 @@ fn print<'a>(
 
         Pattern::Discard { .. } => "_".to_doc(),
 
-        Pattern::VarUsage {
-            name, constructor, ..
-        } => {
-            let v = &constructor
-                .as_ref()
-                .expect("Constructor not found for variable usage")
-                .variant;
-            match v {
-                ValueConstructorVariant::ModuleConstant { literal, .. } => {
-                    const_inline(literal, env)
-                }
-                _ => env.local_var_name(name),
-            }
-        }
+        Pattern::BitArraySize(size) => match size {
+            BitArraySize::Int { .. }
+            | BitArraySize::Variable { .. }
+            | BitArraySize::Block { .. } => bit_array_size(size, env),
+            BitArraySize::BinaryOperator { .. } => bit_array_size(size, env).surround("(", ")"),
+        },
 
         Pattern::Variable { name, .. } => {
             vars.push(name);
@@ -142,6 +134,90 @@ fn print<'a>(
     }
 }
 
+fn bit_array_size<'a>(size: &'a TypedBitArraySize, env: &mut Env<'a>) -> Document<'a> {
+    match size {
+        BitArraySize::Int { value, .. } => int(value),
+        BitArraySize::Variable {
+            name, constructor, ..
+        } => {
+            let v = &constructor
+                .as_ref()
+                .expect("Constructor not found for variable usage")
+                .variant;
+            match v {
+                ValueConstructorVariant::ModuleConstant { literal, .. } => {
+                    const_inline(literal, env)
+                }
+                _ => env.local_var_name(name),
+            }
+        }
+        BitArraySize::BinaryOperator {
+            operator,
+            left,
+            right,
+            ..
+        } => {
+            let operator = match operator {
+                IntOperator::Add => " + ",
+                IntOperator::Subtract => " - ",
+                IntOperator::Multiply => " * ",
+                IntOperator::Divide => return bit_array_size_divide(left, right, "div", env),
+                IntOperator::Remainder => {
+                    return bit_array_size_divide(left, right, "rem", env);
+                }
+            };
+
+            docvec![
+                bit_array_size(left, env),
+                operator,
+                bit_array_size(right, env)
+            ]
+        }
+        BitArraySize::Block { inner, .. } => bit_array_size(inner, env).surround("(", ")"),
+    }
+}
+
+fn bit_array_size_divide<'a>(
+    left: &'a TypedBitArraySize,
+    right: &'a TypedBitArraySize,
+    operator: &'static str,
+    env: &mut Env<'a>,
+) -> Document<'a> {
+    if right.non_zero_compile_time_number() {
+        return bit_array_size_operator(left, operator, right, env);
+    }
+
+    let left = bit_array_size(left, env);
+    let right = bit_array_size(right, env);
+    let denominator = env.next_local_var_name("gleam@denominator");
+    let clauses = docvec![
+        line(),
+        "0 -> 0;",
+        line(),
+        denominator.clone(),
+        " -> ",
+        binop_documents(left, operator, denominator)
+    ];
+    docvec!["case ", right, " of", clauses.nest(INDENT), line(), "end"]
+}
+
+fn bit_array_size_operator<'a>(
+    left: &'a TypedBitArraySize,
+    operator: &'static str,
+    right: &'a TypedBitArraySize,
+    env: &mut Env<'a>,
+) -> Document<'a> {
+    let left = match left {
+        BitArraySize::BinaryOperator { .. } => bit_array_size(left, env).surround("(", ")"),
+        _ => bit_array_size(left, env),
+    };
+    let right = match right {
+        BitArraySize::BinaryOperator { .. } => bit_array_size(right, env).surround("(", ")"),
+        _ => bit_array_size(right, env),
+    };
+    binop_documents(left, operator, right)
+}
+
 pub(super) fn to_doc<'a>(
     p: &'a TypedPattern,
     vars: &mut Vec<&'a str>,
diff --git a/compiler-core/src/exhaustiveness.rs b/compiler-core/src/exhaustiveness.rs
index c4db87a32fd..03a2231f52d 100644
--- a/compiler-core/src/exhaustiveness.rs
+++ b/compiler-core/src/exhaustiveness.rs
@@ -63,7 +63,10 @@ mod missing_patterns;
 pub mod printer;
 
 use crate::{
-    ast::{self, AssignName, Endianness, TypedClause, TypedPattern, TypedPatternBitArraySegment},
+    ast::{
+        self, AssignName, BitArraySize, Endianness, IntOperator, TypedBitArraySize, TypedClause,
+        TypedPattern, TypedPatternBitArraySegment,
+    },
     strings::{convert_string_escape_chars, length_utf16, length_utf32},
     type_::{
         Environment, Opaque, Type, TypeValueConstructor, TypeValueConstructorField, TypeVar,
@@ -859,8 +862,8 @@ pub enum BitArrayTest {
     /// pattern where the size is a variable with a negative value will never
     /// match. So we check this to make sure the test will fail.
     ///
-    VariableIsNotNegative {
-        variable: VariableUsage,
+    ReadSizeIsNotNegative {
+        size: ReadSize,
     },
 
     /// This test checks that the segment read by the given read action is a
@@ -868,7 +871,7 @@ pub enum BitArrayTest {
     ///
     /// We need this check as `NaN` and `Infinity` will not match with float
     /// segments (like `<<_:32-float>>`) on the Erlang target and we must
-    /// replicate the same behavious on the JavaScript target as well.
+    /// replicate the same behaviours on the JavaScript target as well.
     ///
     SegmentIsFiniteFloat {
         read_action: ReadAction,
@@ -888,12 +891,11 @@ impl BitArrayTest {
 
     pub(crate) fn referenced_segment_patterns(&self) -> Vec<(&EcoString, &ReadAction)> {
         match self {
-            BitArrayTest::VariableIsNotNegative { variable } => match variable {
-                VariableUsage::PatternSegment(segment_value, read_action) => {
-                    vec![(segment_value, read_action)]
-                }
-                VariableUsage::OutsideVariable(..) => vec![],
-            },
+            BitArrayTest::ReadSizeIsNotNegative { size } => {
+                let mut references = Vec::new();
+                size.referenced_segment_patterns(&mut references);
+                references
+            }
 
             BitArrayTest::Size(SizeTest { operator: _, size })
             | BitArrayTest::CatchAllIsBytes { size_so_far: size } => {
@@ -909,18 +911,7 @@ impl BitArrayTest {
             }) => {
                 let mut references = vec![];
                 references.append(&mut from.referenced_segment_patterns());
-                match size {
-                    ReadSize::VariableBits { variable, unit: _ } => match variable.as_ref() {
-                        VariableUsage::PatternSegment(segment_value, read_action) => {
-                            references.push((segment_value, read_action))
-                        }
-                        VariableUsage::OutsideVariable(..) => (),
-                    },
-
-                    ReadSize::ConstantBits(..)
-                    | ReadSize::RemainingBits
-                    | ReadSize::RemainingBytes => (),
-                };
+                size.referenced_segment_patterns(&mut references);
 
                 references
             }
@@ -1219,10 +1210,32 @@ impl Confidence {
     }
 }
 
+/// An offset, in bits, into a bit array. An offset contains three parts. For
+/// example, in the following pattern:
+/// ```txt
+/// <<a, b:size(a), c:size(b - 1), payload:size(c)>>
+/// ```
+///
+/// The start of the `payload` segment is the sum of the length of `a` (which we
+/// know is 1 byte), plus the size of `b`, which is the value of `a`, plus the
+/// size of `c`, which is 1 less than the value of `b`.
+///
+/// Here, `constant` would be `8`; `variables` would map `a` to `1`, because it
+/// is referenced once, in a segment with unit `1`; `calculations` would contain
+/// `b - 1`.
+///
 #[derive(Clone, Eq, PartialEq, Debug, Hash)]
 pub struct Offset {
     pub constant: BigInt,
     pub variables: im::HashMap<VariableUsage, usize>,
+    pub calculations: im::Vector<OffsetCalculation>,
+}
+
+#[derive(Clone, Eq, PartialEq, Debug, Hash)]
+pub struct OffsetCalculation {
+    pub left: Offset,
+    pub right: Offset,
+    pub operator: IntOperator,
 }
 
 impl Offset {
@@ -1230,14 +1243,20 @@ impl Offset {
         Self {
             constant: value.into(),
             variables: im::HashMap::new(),
+            calculations: im::Vector::new(),
         }
     }
 
-    fn add_size(self, size: &ReadSize) -> Offset {
+    fn from_size(size: &ReadSize) -> Self {
+        Self::constant(0).add_size(size)
+    }
+
+    fn add_size(mut self, size: &ReadSize) -> Offset {
         match size {
             ReadSize::ConstantBits(value) => Self {
                 constant: self.constant + value,
                 variables: self.variables,
+                calculations: self.calculations,
             },
             ReadSize::VariableBits { variable, unit } => Self {
                 constant: self.constant,
@@ -1248,8 +1267,25 @@ impl Offset {
                     },
                     variable.as_ref().clone(),
                 ),
+                calculations: self.calculations,
             },
             ReadSize::RemainingBits | ReadSize::RemainingBytes => self,
+
+            ReadSize::BinaryOperator {
+                left,
+                right,
+                operator,
+            } => match operator {
+                IntOperator::Add => self.add_size(left).add_size(right),
+                _ => {
+                    self.calculations.push_back(OffsetCalculation {
+                        left: Self::from_size(left),
+                        right: Self::from_size(right),
+                        operator: *operator,
+                    });
+                    self
+                }
+            },
         }
     }
 
@@ -1257,6 +1293,7 @@ impl Offset {
         Offset {
             constant: self.constant.clone() + constant.into(),
             variables: self.variables.clone(),
+            calculations: self.calculations.clone(),
         }
     }
 
@@ -1264,7 +1301,12 @@ impl Offset {
     /// another one.
     ///
     fn greater(&self, other: &Offset) -> Confidence {
-        if self.constant > other.constant && superset(&self.variables, &other.variables) {
+        // We can't easily tell if one calculation is greater than another, so
+        // we can only be certain about one being greater if there are no calculations
+        if self.constant > other.constant
+            && self.calculations.is_empty()
+            && superset(&self.variables, &other.variables)
+        {
             Confidence::Certain
         } else {
             Confidence::Uncertain
@@ -1275,8 +1317,11 @@ impl Offset {
     /// to another one.
     ///
     fn greater_equal(&self, other: &Offset) -> Confidence {
+        // Like `greater`, we can only be certain if there are no calculations
         if self == other
-            || (self.constant >= other.constant && superset(&self.variables, &other.variables))
+            || (self.constant >= other.constant
+                && self.calculations.is_empty()
+                && superset(&self.variables, &other.variables))
         {
             Confidence::Certain
         } else {
@@ -1292,13 +1337,13 @@ impl Offset {
     }
 
     pub(crate) fn is_zero(&self) -> bool {
-        self.constant == BigInt::ZERO && self.variables.is_empty()
+        self.constant == BigInt::ZERO && self.variables.is_empty() && self.calculations.is_empty()
     }
 
     /// If this offset has a constant size, returns its size in bits.
     ///
     pub(crate) fn constant_bits(&self) -> Option<BigInt> {
-        if self.variables.is_empty() {
+        if self.variables.is_empty() && self.calculations.is_empty() {
             Some(self.constant.clone())
         } else {
             None
@@ -1318,15 +1363,23 @@ impl Offset {
     }
 
     fn referenced_segment_patterns(&self) -> Vec<(&EcoString, &ReadAction)> {
-        self.variables
-            .keys()
-            .filter_map(|variable_usage| match variable_usage {
+        let mut references = Vec::new();
+
+        for variable_usage in self.variables.keys() {
+            match variable_usage {
                 VariableUsage::PatternSegment(segment_name, read_action) => {
-                    Some((segment_name, read_action))
+                    references.push((segment_name, read_action));
                 }
-                VariableUsage::OutsideVariable(..) => None,
-            })
-            .collect_vec()
+                VariableUsage::OutsideVariable(..) => {}
+            }
+        }
+
+        for calculation in self.calculations.iter() {
+            references.extend(calculation.left.referenced_segment_patterns());
+            references.extend(calculation.right.referenced_segment_patterns());
+        }
+
+        references
     }
 }
 
@@ -1356,6 +1409,21 @@ pub enum ReadSize {
         variable: Box<VariableUsage>,
         unit: u8,
     },
+
+    /// A maths expression calculating the read size from one or more variables.
+    ///
+    /// ```txt
+    /// <<len, payload:size(len * 8)>>
+    ///        ─┬───────────────────
+    ///         ╰─ Here we have to calculate the length by performing the
+    ///            multiplication at runtime
+    /// ```
+    BinaryOperator {
+        left: Box<ReadSize>,
+        right: Box<ReadSize>,
+        operator: IntOperator,
+    },
+
     /// Read all the remaining bits in the bit array when using a catch all
     /// pattern.
     ///
@@ -1374,9 +1442,10 @@ impl ReadSize {
     pub(crate) fn constant_bits(&self) -> Option<BigInt> {
         match self {
             ReadSize::ConstantBits(value) => Some(value.clone()),
-            ReadSize::VariableBits { .. } | ReadSize::RemainingBits | ReadSize::RemainingBytes => {
-                None
-            }
+            ReadSize::VariableBits { .. }
+            | ReadSize::BinaryOperator { .. }
+            | ReadSize::RemainingBits
+            | ReadSize::RemainingBytes => None,
         }
     }
 
@@ -1389,6 +1458,47 @@ impl ReadSize {
             None
         }
     }
+
+    fn referenced_segment_patterns<'a>(
+        &'a self,
+        references: &mut Vec<(&'a EcoString, &'a ReadAction)>,
+    ) {
+        match self {
+            ReadSize::VariableBits { variable, unit: _ } => match variable.as_ref() {
+                VariableUsage::PatternSegment(segment_value, read_action) => {
+                    references.push((segment_value, read_action));
+                }
+                VariableUsage::OutsideVariable(..) => (),
+            },
+
+            ReadSize::BinaryOperator { left, right, .. } => {
+                left.referenced_segment_patterns(references);
+                right.referenced_segment_patterns(references);
+            }
+
+            ReadSize::ConstantBits(..) | ReadSize::RemainingBits | ReadSize::RemainingBytes => (),
+        };
+    }
+
+    fn can_be_negative(&self) -> bool {
+        match self {
+            ReadSize::ConstantBits(value) => *value < BigInt::ZERO,
+            ReadSize::VariableBits { variable, .. } => match variable.as_ref() {
+                VariableUsage::PatternSegment(_, read_action) => read_action.signed,
+                VariableUsage::OutsideVariable(_) => true,
+            },
+            ReadSize::BinaryOperator {
+                left,
+                right,
+                operator,
+            } => {
+                *operator == IntOperator::Subtract
+                    || left.can_be_negative()
+                    || right.can_be_negative()
+            }
+            ReadSize::RemainingBits | ReadSize::RemainingBytes => false,
+        }
+    }
 }
 
 #[derive(Clone, Eq, PartialEq, Debug, Hash)]
@@ -2784,8 +2894,8 @@ impl CaseToCompile {
                 })
             }
 
-            TypedPattern::VarUsage { .. } => {
-                unreachable!("Cannot convert VarUsage to exhaustiveness pattern")
+            TypedPattern::BitArraySize { .. } => {
+                unreachable!("Cannot convert BitArraySize to exhaustiveness pattern")
             }
         }
     }
@@ -2819,20 +2929,10 @@ impl CaseToCompile {
 
             // If we're reading a variable number of bits we need to make sure
             // that that variable is not negative!
-            if let ReadSize::VariableBits { variable, .. } = &segment_size {
-                match variable.as_ref() {
-                    // If the size variable comes from reading an unsigned
-                    // number we know that it can't be negative! So we can skip
-                    // checking it.
-                    VariableUsage::PatternSegment(_, read_action) if !read_action.signed => (),
-
-                    // Otherwise we must make sure that the read size is not
-                    // negative.
-                    VariableUsage::PatternSegment(..) | VariableUsage::OutsideVariable(_) => tests
-                        .push_back(BitArrayTest::VariableIsNotNegative {
-                            variable: variable.as_ref().clone(),
-                        }),
-                }
+            if segment_size.can_be_negative() {
+                tests.push_back(BitArrayTest::ReadSizeIsNotNegative {
+                    size: segment_size.clone(),
+                });
             }
 
             // All segments but the last will require the original bit array to
@@ -2957,22 +3057,9 @@ fn segment_size(
     let pattern = pattern.unwrap_or(&segment.value);
 
     match segment.size() {
-        // Size could either be a constant or a variable usage. In either case
-        // we need to take the segment's unit into account!
-        Some(ast::Pattern::Int { int_value, .. }) => {
-            ReadSize::ConstantBits(int_value * segment.unit())
-        }
-        Some(ast::Pattern::VarUsage { name, .. }) => {
-            let variable = match pattern_variables.get(name) {
-                Some(read_action) => {
-                    VariableUsage::PatternSegment(name.clone(), read_action.clone())
-                }
-                None => VariableUsage::OutsideVariable(name.clone()),
-            };
-            ReadSize::VariableBits {
-                variable: Box::new(variable),
-                unit: segment.unit(),
-            }
+        // The size of a segment must be a `BitArraySize` pattern.
+        Some(ast::Pattern::BitArraySize(size)) => {
+            bit_array_size(segment.unit(), pattern_variables, size)
         }
         Some(x) => panic!("invalid pattern size made it to code generation {x:?}"),
 
@@ -3013,6 +3100,51 @@ fn segment_size(
     }
 }
 
+fn bit_array_size(
+    unit: u8,
+    pattern_variables: &HashMap<EcoString, ReadAction>,
+    size: &TypedBitArraySize,
+) -> ReadSize {
+    match size {
+        BitArraySize::Int { int_value, .. } => ReadSize::ConstantBits(int_value * unit),
+        BitArraySize::Variable { name, .. } => {
+            let variable = match pattern_variables.get(name) {
+                Some(read_action) => {
+                    VariableUsage::PatternSegment(name.clone(), read_action.clone())
+                }
+                None => VariableUsage::OutsideVariable(name.clone()),
+            };
+            ReadSize::VariableBits {
+                variable: Box::new(variable),
+                unit,
+            }
+        }
+        BitArraySize::BinaryOperator {
+            operator,
+            left,
+            right,
+            ..
+        } => {
+            let size = ReadSize::BinaryOperator {
+                left: Box::new(bit_array_size(1, pattern_variables, left)),
+                right: Box::new(bit_array_size(1, pattern_variables, right)),
+                operator: *operator,
+            };
+
+            if unit == 1 {
+                size
+            } else {
+                ReadSize::BinaryOperator {
+                    left: Box::new(size),
+                    right: Box::new(ReadSize::ConstantBits(unit.into())),
+                    operator: IntOperator::Multiply,
+                }
+            }
+        }
+        BitArraySize::Block { inner, .. } => bit_array_size(unit, pattern_variables, inner),
+    }
+}
+
 /// Returns `true` if one bag is a superset of the other: that is it contains
 /// all the keys of `other` in a quantity that's greater or equal.
 ///
diff --git a/compiler-core/src/format.rs b/compiler-core/src/format.rs
index 65c108efea5..3c4aa137c0b 100644
--- a/compiler-core/src/format.rs
+++ b/compiler-core/src/format.rs
@@ -2231,7 +2231,7 @@ impl<'comments> Formatter<'comments> {
 
             Pattern::Variable { name, .. } => name.to_doc(),
 
-            Pattern::VarUsage { name, .. } => name.to_doc(),
+            Pattern::BitArraySize(size) => self.bit_array_size(size),
 
             Pattern::Assign { name, pattern, .. } => {
                 self.pattern(pattern).append(" as ").append(name.as_str())
@@ -2295,6 +2295,34 @@ impl<'comments> Formatter<'comments> {
         commented(doc, comments)
     }
 
+    fn bit_array_size<'a>(&mut self, size: &'a BitArraySize<()>) -> Document<'a> {
+        match size {
+            BitArraySize::Int { value, .. } => self.int(value),
+            BitArraySize::Variable { name, .. } => name.to_doc(),
+            BitArraySize::BinaryOperator {
+                left,
+                right,
+                operator,
+                ..
+            } => {
+                let operator = match operator {
+                    IntOperator::Add => " + ",
+                    IntOperator::Subtract => " - ",
+                    IntOperator::Multiply => " * ",
+                    IntOperator::Divide => " / ",
+                    IntOperator::Remainder => " % ",
+                };
+
+                docvec![
+                    self.bit_array_size(left),
+                    operator,
+                    self.bit_array_size(right)
+                ]
+            }
+            BitArraySize::Block { inner, .. } => self.bit_array_size(inner).surround("{ ", " }"),
+        }
+    }
+
     fn list_pattern<'a>(
         &mut self,
         elements: &'a [UntypedPattern],
diff --git a/compiler-core/src/javascript/decision.rs b/compiler-core/src/javascript/decision.rs
index d17a64eae07..daa5ab09ae2 100644
--- a/compiler-core/src/javascript/decision.rs
+++ b/compiler-core/src/javascript/decision.rs
@@ -993,8 +993,8 @@ impl<'generator, 'module, 'a> Variables<'generator, 'module, 'a> {
                     }
                 }
 
-                BitArrayTest::VariableIsNotNegative { variable } => {
-                    docvec![self.local_var(variable.name()), " >= 0"]
+                BitArrayTest::ReadSizeIsNotNegative { size } => {
+                    docvec![self.read_size_to_doc(size), " >= 0"]
                 }
 
                 BitArrayTest::SegmentIsFiniteFloat {
@@ -1204,6 +1204,24 @@ impl<'generator, 'module, 'a> Variables<'generator, 'module, 'a> {
             pieces.push(variable.to_doc())
         }
 
+        for calculation in offset.calculations.iter() {
+            let left = self.offset_to_doc(&calculation.left, true);
+            let right = self.offset_to_doc(&calculation.right, true);
+
+            let calculation = self.expression_generator.bin_op_with_doc_operands(
+                calculation.operator.to_bin_op(),
+                left,
+                right,
+                &crate::type_::int(),
+            );
+
+            if parenthesise {
+                pieces.push(calculation.surround("(", ")"))
+            } else {
+                pieces.push(calculation)
+            }
+        }
+
         if pieces.len() > 1 && parenthesise {
             docvec!["(", join(pieces, " + ".to_doc()), ")"]
         } else {
@@ -1222,10 +1240,43 @@ impl<'generator, 'module, 'a> Variables<'generator, 'module, 'a> {
                 Some(if *unit == 1 {
                     variable.to_doc()
                 } else {
-                    docvec![variable, " * ", unit]
+                    docvec![variable, " * ", *unit as i64]
                 })
             }
             ReadSize::RemainingBits | ReadSize::RemainingBytes => None,
+
+            ReadSize::BinaryOperator {
+                left,
+                right,
+                operator,
+            } => {
+                let left = if self.read_size_must_be_wrapped(left) {
+                    self.read_size_to_doc(left)?.surround("(", ")")
+                } else {
+                    self.read_size_to_doc(left)?
+                };
+                let right = if self.read_size_must_be_wrapped(right) {
+                    self.read_size_to_doc(right)?.surround("(", ")")
+                } else {
+                    self.read_size_to_doc(right)?
+                };
+
+                Some(self.expression_generator.bin_op_with_doc_operands(
+                    operator.to_bin_op(),
+                    left,
+                    right,
+                    &crate::type_::int(),
+                ))
+            }
+        }
+    }
+
+    fn read_size_must_be_wrapped(&self, size: &ReadSize) -> bool {
+        match size {
+            ReadSize::ConstantBits(_) | ReadSize::RemainingBits | ReadSize::RemainingBytes => false,
+
+            ReadSize::VariableBits { unit, .. } => *unit != 1,
+            ReadSize::BinaryOperator { .. } => true,
         }
     }
 
diff --git a/compiler-core/src/javascript/expression.rs b/compiler-core/src/javascript/expression.rs
index c7715b47f7e..343edbb5a3b 100644
--- a/compiler-core/src/javascript/expression.rs
+++ b/compiler-core/src/javascript/expression.rs
@@ -1523,7 +1523,7 @@ impl<'module, 'a> Generator<'module, 'a> {
         docvec![left, " ", op, " ", right]
     }
 
-    fn bin_op_with_doc_operands(
+    pub(super) fn bin_op_with_doc_operands(
         &mut self,
         name: BinOp,
         left: Document<'a>,
@@ -1549,11 +1549,11 @@ impl<'module, 'a> Generator<'module, 'a> {
                 docvec!["remainderInt", wrap_args([left, right])]
             }
             BinOp::DivInt => {
-                self.tracker.int_remainder_used = true;
+                self.tracker.int_division_used = true;
                 docvec!["divideInt", wrap_args([left, right])]
             }
             BinOp::DivFloat => {
-                self.tracker.int_remainder_used = true;
+                self.tracker.float_division_used = true;
                 docvec!["divideFloat", wrap_args([left, right])]
             }
         }
diff --git a/compiler-core/src/language_server/reference.rs b/compiler-core/src/language_server/reference.rs
index ce1e8e95ecd..b43c07b7540 100644
--- a/compiler-core/src/language_server/reference.rs
+++ b/compiler-core/src/language_server/reference.rs
@@ -6,7 +6,7 @@ use lsp_types::Location;
 use crate::{
     analyse,
     ast::{
-        self, ArgNames, CustomType, Definition, Function, ModuleConstant, Pattern,
+        self, ArgNames, BitArraySize, CustomType, Definition, Function, ModuleConstant, Pattern,
         RecordConstructor, SrcSpan, TypedExpr, TypedModule, visit::Visit,
     },
     build::Located,
@@ -80,12 +80,12 @@ pub fn reference_for_ast_node(
             origin: Some(origin.clone()),
             name: name.clone(),
         }),
-        Located::Pattern(Pattern::VarUsage {
+        Located::Pattern(Pattern::BitArraySize(BitArraySize::Variable {
             constructor,
             location,
             name,
             ..
-        }) => constructor
+        })) => constructor
             .as_ref()
             .and_then(|constructor| match &constructor.variant {
                 ValueConstructorVariant::LocalVariable {
@@ -477,11 +477,11 @@ impl<'ast> Visit<'ast> for FindVariableReferences {
         }
     }
 
-    fn visit_typed_pattern_var_usage(
+    fn visit_typed_bit_array_size_variable(
         &mut self,
         location: &'ast SrcSpan,
         _name: &'ast EcoString,
-        constructor: &'ast Option<ValueConstructor>,
+        constructor: &'ast Option<Box<ValueConstructor>>,
         _type_: &'ast std::sync::Arc<Type>,
     ) {
         let variant = match constructor {
diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs
index 96743c4d251..172c119e84b 100644
--- a/compiler-core/src/parse.rs
+++ b/compiler-core/src/parse.rs
@@ -58,14 +58,14 @@ use crate::Warning;
 use crate::analyse::Inferred;
 use crate::ast::{
     Arg, ArgNames, Assert, AssignName, Assignment, AssignmentKind, BinOp, BitArrayOption,
-    BitArraySegment, CAPTURE_VARIABLE, CallArg, Clause, ClauseGuard, Constant, CustomType,
-    Definition, Function, FunctionLiteralKind, HasLocation, Import, Module, ModuleConstant,
-    Pattern, Publicity, RecordBeingUpdated, RecordConstructor, RecordConstructorArg, SrcSpan,
-    Statement, TargetedDefinition, TodoKind, TypeAlias, TypeAst, TypeAstConstructor, TypeAstFn,
-    TypeAstHole, TypeAstTuple, TypeAstVar, UnqualifiedImport, UntypedArg, UntypedClause,
-    UntypedClauseGuard, UntypedConstant, UntypedDefinition, UntypedExpr, UntypedModule,
-    UntypedPattern, UntypedRecordUpdateArg, UntypedStatement, UntypedUseAssignment, Use,
-    UseAssignment,
+    BitArraySegment, BitArraySize, CAPTURE_VARIABLE, CallArg, Clause, ClauseGuard, Constant,
+    CustomType, Definition, Function, FunctionLiteralKind, HasLocation, Import, IntOperator,
+    Module, ModuleConstant, Pattern, Publicity, RecordBeingUpdated, RecordConstructor,
+    RecordConstructorArg, SrcSpan, Statement, TargetedDefinition, TodoKind, TypeAlias, TypeAst,
+    TypeAstConstructor, TypeAstFn, TypeAstHole, TypeAstTuple, TypeAstVar, UnqualifiedImport,
+    UntypedArg, UntypedClause, UntypedClauseGuard, UntypedConstant, UntypedDefinition, UntypedExpr,
+    UntypedModule, UntypedPattern, UntypedRecordUpdateArg, UntypedStatement, UntypedUseAssignment,
+    Use, UseAssignment,
 };
 use crate::build::Target;
 use crate::error::wrap;
@@ -1393,7 +1393,7 @@ where
                                 x => x,
                             },
                             &Parser::expect_bit_array_pattern_segment_arg,
-                            &bit_array_pattern_int,
+                            &bit_array_size_int,
                         )
                     },
                     Some(&Token::Comma),
@@ -3402,19 +3402,129 @@ where
     }
 
     fn expect_bit_array_pattern_segment_arg(&mut self) -> Result<UntypedPattern, ParseError> {
-        match self.next_tok() {
-            Some((start, Token::Name { name }, end)) => Ok(Pattern::VarUsage {
+        Ok(Pattern::BitArraySize(self.expect_bit_array_size()?))
+    }
+
+    fn expect_bit_array_size(&mut self) -> Result<BitArraySize<()>, ParseError> {
+        let left = match self.next_tok() {
+            Some((start, Token::Name { name }, end)) => BitArraySize::Variable {
                 location: SrcSpan { start, end },
                 name,
                 constructor: None,
                 type_: (),
-            }),
-            Some((start, Token::Int { value, int_value }, end)) => Ok(Pattern::Int {
+            },
+            Some((start, Token::Int { value, int_value }, end)) => BitArraySize::Int {
                 location: SrcSpan { start, end },
                 value,
                 int_value,
-            }),
-            _ => self.next_tok_unexpected(vec!["A variable name or an integer".into()]),
+            },
+            Some((start, Token::LeftBrace, _)) => {
+                let inner = self.expect_bit_array_size()?;
+                let (_, end) = self.expect_one(&Token::RightBrace)?;
+
+                BitArraySize::Block {
+                    location: SrcSpan { start, end },
+                    inner: Box::new(inner),
+                }
+            }
+            _ => return self.next_tok_unexpected(vec!["A variable name or an integer".into()]),
+        };
+
+        let Some((start, token, end)) = self.tok0.take() else {
+            return Ok(left);
+        };
+
+        match token {
+            Token::Plus => {
+                _ = self.next_tok();
+                let right = self.expect_bit_array_size()?;
+
+                Ok(self.bit_array_size_binary_operator(left, right, IntOperator::Add))
+            }
+            Token::Minus => {
+                _ = self.next_tok();
+                let right = self.expect_bit_array_size()?;
+
+                Ok(self.bit_array_size_binary_operator(left, right, IntOperator::Subtract))
+            }
+            Token::Star => {
+                _ = self.next_tok();
+                let right = self.expect_bit_array_size()?;
+
+                Ok(
+                    self.bit_array_size_high_precedence_operator(
+                        left,
+                        right,
+                        IntOperator::Multiply,
+                    ),
+                )
+            }
+            Token::Slash => {
+                _ = self.next_tok();
+                let right = self.expect_bit_array_size()?;
+
+                Ok(self.bit_array_size_high_precedence_operator(left, right, IntOperator::Divide))
+            }
+            Token::Percent => {
+                _ = self.next_tok();
+                let right = self.expect_bit_array_size()?;
+
+                Ok(self.bit_array_size_high_precedence_operator(
+                    left,
+                    right,
+                    IntOperator::Remainder,
+                ))
+            }
+            _ => {
+                self.tok0 = Some((start, token, end));
+                Ok(left)
+            }
+        }
+    }
+
+    fn bit_array_size_binary_operator(
+        &self,
+        left: BitArraySize<()>,
+        right: BitArraySize<()>,
+        operator: IntOperator,
+    ) -> BitArraySize<()> {
+        let start = left.location().start;
+        let end = right.location().end;
+
+        BitArraySize::BinaryOperator {
+            left: Box::new(left),
+            right: Box::new(right),
+            operator,
+            location: SrcSpan { start, end },
+        }
+    }
+
+    fn bit_array_size_high_precedence_operator(
+        &self,
+        left: BitArraySize<()>,
+        right: BitArraySize<()>,
+        operator: IntOperator,
+    ) -> BitArraySize<()> {
+        match right {
+            BitArraySize::Int { .. }
+            | BitArraySize::Variable { .. }
+            | BitArraySize::Block { .. } => {
+                self.bit_array_size_binary_operator(left, right, operator)
+            }
+            // This operator has the highest precedence of the possible operators
+            // for bit array size patterns. So, `a * b + c` should be parsed as
+            // `(a * b) + c`. If the right-hand side of this operator is another
+            // operator, we rearrange it to ensure correct precedence.
+            BitArraySize::BinaryOperator {
+                operator: right_operator,
+                left: middle,
+                right,
+                ..
+            } => self.bit_array_size_binary_operator(
+                self.bit_array_size_binary_operator(left, *middle, operator),
+                *right,
+                right_operator,
+            ),
         }
     }
 
@@ -4276,17 +4386,12 @@ fn clause_guard_reduction(
 // BitArrays in patterns, guards, and expressions have a very similar structure
 // but need specific types. These are helpers for that. There is probably a
 // rustier way to do this :)
-fn bit_array_pattern_int(
-    value: EcoString,
-    int_value: BigInt,
-    start: u32,
-    end: u32,
-) -> UntypedPattern {
-    Pattern::Int {
+fn bit_array_size_int(value: EcoString, int_value: BigInt, start: u32, end: u32) -> UntypedPattern {
+    Pattern::BitArraySize(BitArraySize::Int {
         location: SrcSpan { start, end },
         value,
         int_value,
-    }
+    })
 }
 
 fn bit_array_expr_int(value: EcoString, int_value: BigInt, start: u32, end: u32) -> UntypedExpr {
diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs
index 69ffe77acae..8445a99a889 100644
--- a/compiler-core/src/type_/expression.rs
+++ b/compiler-core/src/type_/expression.rs
@@ -4765,7 +4765,7 @@ impl UseAssignments {
                 pattern @ (Pattern::Int { .. }
                 | Pattern::Float { .. }
                 | Pattern::String { .. }
-                | Pattern::VarUsage { .. }
+                | Pattern::BitArraySize { .. }
                 | Pattern::Assign { .. }
                 | Pattern::List { .. }
                 | Pattern::Constructor { .. }
diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs
index b1926bb9332..2dea66f03c1 100644
--- a/compiler-core/src/type_/pattern.rs
+++ b/compiler-core/src/type_/pattern.rs
@@ -11,7 +11,8 @@ use super::*;
 use crate::{
     analyse::{self, Inferred, name::check_name_case},
     ast::{
-        AssignName, BitArrayOption, ImplicitCallArgOrigin, Layer, UntypedPatternBitArraySegment,
+        AssignName, BitArrayOption, BitArraySize, ImplicitCallArgOrigin, Layer, TypedBitArraySize,
+        UntypedPatternBitArraySegment,
     },
     parse::PatternPosition,
     reference::ReferenceKind,
@@ -493,11 +494,11 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                     // Int segments that aren't a whole number of bytes
                     BitArrayOption::<TypedPattern>::Size { value, .. } if segment_type.is_int() => {
                         match &**value {
-                            Pattern::<_>::Int {
+                            Pattern::BitArraySize(BitArraySize::Int {
                                 location,
                                 int_value,
                                 ..
-                            } if int_value % 8 != BigInt::ZERO => {
+                            }) if int_value % 8 != BigInt::ZERO => {
                                 self.track_feature_usage(
                                     FeatureKind::JavaScriptUnalignedBitArray,
                                     *location,
@@ -620,55 +621,14 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                 }
             }
 
-            Pattern::VarUsage { name, location, .. } => {
-                let constructor = match self.variables.get_mut(&name) {
-                    // If we've bound a variable in the current bit array pattern,
-                    // we want to use that.
-                    Some(variable) if variable.in_scope() => {
-                        variable.usage = Usage::UsedInPattern;
-                        ValueConstructor::local_variable(
-                            variable.location,
-                            variable.origin.clone(),
-                            variable.type_.clone(),
-                        )
+            Pattern::BitArraySize(size) => {
+                let location = size.location();
+                match self.bit_array_size(size, type_.clone()) {
+                    Ok(size) => Pattern::BitArraySize(size),
+                    Err(error) => {
+                        self.error(error);
+                        Pattern::Invalid { location, type_ }
                     }
-                    // Otherwise, we check the local scope.
-                    Some(_) | None => match self.environment.get_variable(&name) {
-                        Some(constructor) => constructor.clone(),
-                        None => {
-                            self.error(Error::UnknownVariable {
-                                location,
-                                name: name.clone(),
-                                variables: self.environment.local_value_names(),
-                                discarded_location: self
-                                    .environment
-                                    .discarded_names
-                                    .get(&eco_format!("_{name}"))
-                                    .cloned(),
-                                type_with_name_in_scope: self
-                                    .environment
-                                    .module_types
-                                    .keys()
-                                    .any(|type_| type_ == &name),
-                            });
-                            return Pattern::Invalid { location, type_ };
-                        }
-                    },
-                };
-
-                self.environment.increment_usage(&name);
-                let type_ = self.environment.instantiate(
-                    constructor.type_.clone(),
-                    &mut hashmap![],
-                    self.hydrator,
-                );
-                self.unify_types(int(), type_.clone(), location);
-
-                Pattern::VarUsage {
-                    name,
-                    location,
-                    constructor: Some(constructor),
-                    type_,
                 }
             }
 
@@ -1237,6 +1197,101 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
             .collect()
     }
 
+    fn bit_array_size(
+        &mut self,
+        size: BitArraySize<()>,
+        type_: Arc<Type>,
+    ) -> Result<TypedBitArraySize, Error> {
+        let typed_size = match size {
+            BitArraySize::Int {
+                location,
+                value,
+                int_value,
+            } => {
+                self.unify_types(type_, int(), location);
+
+                if self.environment.target == Target::JavaScript
+                    && !self.current_function.has_javascript_external
+                {
+                    check_javascript_int_safety(&int_value, location, self.problems);
+                }
+
+                BitArraySize::Int {
+                    location,
+                    value,
+                    int_value,
+                }
+            }
+            BitArraySize::Variable { name, location, .. } => {
+                let constructor = match self.variables.get_mut(&name) {
+                    // If we've bound a variable in the current bit array pattern,
+                    // we want to use that.
+                    Some(variable) if variable.in_scope() => {
+                        variable.usage = Usage::UsedInPattern;
+                        ValueConstructor::local_variable(
+                            variable.location,
+                            variable.origin.clone(),
+                            variable.type_.clone(),
+                        )
+                    }
+                    // Otherwise, we check the local scope.
+                    Some(_) | None => match self.environment.get_variable(&name) {
+                        Some(constructor) => constructor.clone(),
+                        None => {
+                            return Err(Error::UnknownVariable {
+                                location,
+                                name: name.clone(),
+                                variables: self.environment.local_value_names(),
+                                discarded_location: self
+                                    .environment
+                                    .discarded_names
+                                    .get(&eco_format!("_{name}"))
+                                    .cloned(),
+                                type_with_name_in_scope: self
+                                    .environment
+                                    .module_types
+                                    .keys()
+                                    .any(|type_| type_ == &name),
+                            });
+                        }
+                    },
+                };
+
+                self.environment.increment_usage(&name);
+                let type_ = self.environment.instantiate(
+                    constructor.type_.clone(),
+                    &mut hashmap![],
+                    self.hydrator,
+                );
+                self.unify_types(int(), type_.clone(), location);
+
+                BitArraySize::Variable {
+                    name,
+                    location,
+                    constructor: Some(Box::new(constructor)),
+                    type_,
+                }
+            }
+            BitArraySize::BinaryOperator {
+                location,
+                operator,
+                left,
+                right,
+            } => BitArraySize::BinaryOperator {
+                location,
+                operator,
+                left: Box::new(self.bit_array_size(*left, type_.clone())?),
+                right: Box::new(self.bit_array_size(*right, type_)?),
+            },
+            BitArraySize::Block { location, inner } => BitArraySize::Block {
+                location,
+                inner: Box::new(self.bit_array_size(*inner, type_)?),
+            },
+        };
+
+        Ok(typed_size)
+    }
+
     fn check_name_case(&mut self, location: SrcSpan, name: &EcoString, kind: Named) {
         if let Err(error) = check_name_case(location, name, kind) {
             self.problems.error(error);


Test patch:
diff --git a/compiler-core/src/erlang/tests/bit_arrays.rs b/compiler-core/src/erlang/tests/bit_arrays.rs
index 664053e3cc9..c29eab740b8 100644
--- a/compiler-core/src/erlang/tests/bit_arrays.rs
+++ b/compiler-core/src/erlang/tests/bit_arrays.rs
@@ -255,3 +255,62 @@ pub fn go(x) {
 "
     );
 }
+
+#[test]
+fn operator_in_pattern_size() {
+    assert_erl!(
+        "
+pub fn main() {
+  let assert <<len, payload:size(len * 8 - 8)>> = <<>>
+}
+"
+    );
+}
+
+#[test]
+fn operator_in_pattern_size2() {
+    assert_erl!(
+        "
+pub fn main() {
+  let assert <<len, payload:size(len / 8 - 1)>> = <<>>
+}
+"
+    );
+}
+
+#[test]
+fn operator_in_pattern_size3() {
+    assert_erl!(
+        "
+pub fn main() {
+  let additional = 10
+  let assert <<len, payload:size(len + additional * 8)>> = <<>>
+}
+"
+    );
+}
+
+#[test]
+fn block_in_pattern_size() {
+    assert_erl!(
+        "
+pub fn main() {
+  let assert <<len, payload:size({ len - 1 } * 8)>> = <<>>
+}
+"
+    );
+}
+
+#[test]
+fn non_byte_aligned_size_calculation() {
+    assert_erl!(
+        "
+pub fn main() {
+  case <<>> {
+    <<a:1, b:3, c:size(b - 2)>> -> c + b
+    _ -> 1
+  }
+}
+"
+    );
+}
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__block_in_pattern_size.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__block_in_pattern_size.snap
new file mode 100644
index 00000000000..4c7b5bca743
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__block_in_pattern_size.snap
@@ -0,0 +1,36 @@
+---
+source: compiler-core/src/erlang/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let assert <<len, payload:size({ len - 1 } * 8)>> = <<>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert <<len, payload:size({ len - 1 } * 8)>> = <<>>
+}
+
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+-define(FILEPATH, "project/test/my/mod.gleam").
+-export([main/0]).
+
+-file("project/test/my/mod.gleam", 2).
+-spec main() -> bitstring().
+main() ->
+    _assert_subject = <<>>,
+    case _assert_subject of
+        <<Len, Payload:((Len - 1) * 8)>> -> _assert_subject;
+        _assert_fail ->
+            erlang:error(#{gleam_error => let_assert,
+                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
+                        file => <<?FILEPATH/utf8>>,
+                        module => <<"my/mod"/utf8>>,
+                        function => <<"main"/utf8>>,
+                        line => 3,
+                        value => _assert_fail,
+                        start => 19,
+                        'end' => 75,
+                        pattern_start => 30,
+                        pattern_end => 68})
+    end.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__non_byte_aligned_size_calculation.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__non_byte_aligned_size_calculation.snap
new file mode 100644
index 00000000000..2a50b30e51f
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__non_byte_aligned_size_calculation.snap
@@ -0,0 +1,30 @@
+---
+source: compiler-core/src/erlang/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  case <<>> {\n    <<a:1, b:3, c:size(b - 2)>> -> c + b\n    _ -> 1\n  }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  case <<>> {
+    <<a:1, b:3, c:size(b - 2)>> -> c + b
+    _ -> 1
+  }
+}
+
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+-define(FILEPATH, "project/test/my/mod.gleam").
+-export([main/0]).
+
+-file("project/test/my/mod.gleam", 2).
+-spec main() -> integer().
+main() ->
+    case <<>> of
+        <<A:1, B:3, C:(B - 2)>> ->
+            C + B;
+
+        _ ->
+            1
+    end.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size.snap
new file mode 100644
index 00000000000..0184908cb52
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size.snap
@@ -0,0 +1,36 @@
+---
+source: compiler-core/src/erlang/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let assert <<len, payload:size(len * 8 - 8)>> = <<>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert <<len, payload:size(len * 8 - 8)>> = <<>>
+}
+
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+-define(FILEPATH, "project/test/my/mod.gleam").
+-export([main/0]).
+
+-file("project/test/my/mod.gleam", 2).
+-spec main() -> bitstring().
+main() ->
+    _assert_subject = <<>>,
+    case _assert_subject of
+        <<Len, Payload:(Len * 8 - 8)>> -> _assert_subject;
+        _assert_fail ->
+            erlang:error(#{gleam_error => let_assert,
+                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
+                        file => <<?FILEPATH/utf8>>,
+                        module => <<"my/mod"/utf8>>,
+                        function => <<"main"/utf8>>,
+                        line => 3,
+                        value => _assert_fail,
+                        start => 19,
+                        'end' => 71,
+                        pattern_start => 30,
+                        pattern_end => 64})
+    end.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size2.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size2.snap
new file mode 100644
index 00000000000..8c9d213d046
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size2.snap
@@ -0,0 +1,36 @@
+---
+source: compiler-core/src/erlang/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let assert <<len, payload:size(len / 8 - 1)>> = <<>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert <<len, payload:size(len / 8 - 1)>> = <<>>
+}
+
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+-define(FILEPATH, "project/test/my/mod.gleam").
+-export([main/0]).
+
+-file("project/test/my/mod.gleam", 2).
+-spec main() -> bitstring().
+main() ->
+    _assert_subject = <<>>,
+    case _assert_subject of
+        <<Len, Payload:(Len div 8 - 1)>> -> _assert_subject;
+        _assert_fail ->
+            erlang:error(#{gleam_error => let_assert,
+                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
+                        file => <<?FILEPATH/utf8>>,
+                        module => <<"my/mod"/utf8>>,
+                        function => <<"main"/utf8>>,
+                        line => 3,
+                        value => _assert_fail,
+                        start => 19,
+                        'end' => 71,
+                        pattern_start => 30,
+                        pattern_end => 64})
+    end.
diff --git a/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size3.snap b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size3.snap
new file mode 100644
index 00000000000..c107431d80c
--- /dev/null
+++ b/compiler-core/src/erlang/tests/snapshots/gleam_core__erlang__tests__bit_arrays__operator_in_pattern_size3.snap
@@ -0,0 +1,38 @@
+---
+source: compiler-core/src/erlang/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let additional = 10\n  let assert <<len, payload:size(len + additional * 8)>> = <<>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let additional = 10
+  let assert <<len, payload:size(len + additional * 8)>> = <<>>
+}
+
+
+----- COMPILED ERLANG
+-module(my@mod).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
+-define(FILEPATH, "project/test/my/mod.gleam").
+-export([main/0]).
+
+-file("project/test/my/mod.gleam", 2).
+-spec main() -> bitstring().
+main() ->
+    Additional = 10,
+    _assert_subject = <<>>,
+    case _assert_subject of
+        <<Len, Payload:(Len + Additional * 8)>> -> _assert_subject;
+        _assert_fail ->
+            erlang:error(#{gleam_error => let_assert,
+                        message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
+                        file => <<?FILEPATH/utf8>>,
+                        module => <<"my/mod"/utf8>>,
+                        function => <<"main"/utf8>>,
+                        line => 4,
+                        value => _assert_fail,
+                        start => 41,
+                        'end' => 102,
+                        pattern_start => 52,
+                        pattern_end => 95})
+    end.
diff --git a/compiler-core/src/format/tests/bit_array.rs b/compiler-core/src/format/tests/bit_array.rs
index 5354f68c270..0204003701b 100644
--- a/compiler-core/src/format/tests/bit_array.rs
+++ b/compiler-core/src/format/tests/bit_array.rs
@@ -202,3 +202,13 @@ fn block_value() {
 "#
     );
 }
+
+#[test]
+fn operator_in_pattern_size() {
+    assert_format!(
+        "pub fn main() {
+  let assert <<len, payload:size({ len + 1 } * 8 + 1)>> = <<>>
+}
+"
+    );
+}
diff --git a/compiler-core/src/javascript/tests/bit_arrays.rs b/compiler-core/src/javascript/tests/bit_arrays.rs
index 35296eaf91f..73a2c3db795 100644
--- a/compiler-core/src/javascript/tests/bit_arrays.rs
+++ b/compiler-core/src/javascript/tests/bit_arrays.rs
@@ -2561,3 +2561,73 @@ pub fn go(codepoint) {
 "
     );
 }
+
+#[test]
+fn operator_in_pattern_size() {
+    assert_js!(
+        "
+pub fn main() {
+  let assert <<len, payload:size(len * 8)>> = <<>>
+}
+"
+    );
+}
+
+#[test]
+fn operator_in_pattern_size2() {
+    assert_js!(
+        "
+pub fn main() {
+  let assert <<len, payload:size(len / 8 - 1)>> = <<>>
+}
+"
+    );
+}
+
+#[test]
+fn operator_in_pattern_size3() {
+    assert_js!(
+        "
+pub fn main() {
+  let additional = 10
+  let assert <<len, payload:size(len + additional * 8)>> = <<>>
+}
+"
+    );
+}
+
+#[test]
+fn block_in_pattern_size() {
+    assert_js!(
+        "
+pub fn main() {
+  let assert <<len, payload:size({ len - 1 } * 8)>> = <<>>
+}
+"
+    );
+}
+
+#[test]
+fn non_byte_aligned_size_calculation() {
+    assert_js!(
+        "
+pub fn main() {
+  case <<>> {
+    <<a:1, b:3, c:size(b - 2)>> -> c + b
+    _ -> 1
+  }
+}
+"
+    );
+}
+
+#[test]
+fn pattern_match_on_negative_size_calculation() {
+    assert_js!(
+        "
+pub fn main() {
+  let assert <<a, b:size(a - 100000), c:size(b)>> = <<1, 2, 3, 4, 5>>
+}
+"
+    );
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__block_in_pattern_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__block_in_pattern_size.snap
new file mode 100644
index 00000000000..d51367623cc
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__block_in_pattern_size.snap
@@ -0,0 +1,49 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let assert <<len, payload:size({ len - 1 } * 8)>> = <<>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert <<len, payload:size({ len - 1 } * 8)>> = <<>>
+}
+
+
+----- COMPILED JAVASCRIPT
+import { makeError, toBitArray, bitArraySliceToInt } from "../gleam.mjs";
+
+const FILEPATH = "src/module.gleam";
+
+export function main() {
+  let $ = toBitArray([]);
+  let len$1;
+  let payload;
+  if ($.bitSize >= 8) {
+    let len = $.byteAt(0);
+    if ((len - 1) * 8 >= 0 && $.bitSize === 8 + (len - 1) * 8) {
+      len$1 = len;
+      payload = bitArraySliceToInt($, 8, 8 + (len$1 - 1) * 8, true, false);
+    } else {
+      throw makeError(
+        "let_assert",
+        FILEPATH,
+        "my/mod",
+        3,
+        "main",
+        "Pattern match failed, no pattern matched the value.",
+        { value: $, start: 19, end: 75, pattern_start: 30, pattern_end: 68 }
+      )
+    }
+  } else {
+    throw makeError(
+      "let_assert",
+      FILEPATH,
+      "my/mod",
+      3,
+      "main",
+      "Pattern match failed, no pattern matched the value.",
+      { value: $, start: 19, end: 75, pattern_start: 30, pattern_end: 68 }
+    )
+  }
+  return $;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_dynamic_size_float_pattern_with_unit.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_dynamic_size_float_pattern_with_unit.snap
index 4531b1fa8e6..28cd9616546 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_dynamic_size_float_pattern_with_unit.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_dynamic_size_float_pattern_with_unit.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 1845
 expression: "\npub fn go(x) {\n  let size = 3\n  case x {\n    <<1.3:size(size)-unit(2)>> -> 1\n    _ -> 2\n  }\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -21,7 +19,7 @@ import { bitArraySliceToFloat } from "../gleam.mjs";
 export function go(x) {
   let size = 3;
   if (
-    size >= 0 &&
+    size * 2 >= 0 &&
     x.bitSize === size * 2 &&
     bitArraySliceToFloat(x, 0, size * 2, true) === 1.3
   ) {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_dynamic_size_pattern_with_unit.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_dynamic_size_pattern_with_unit.snap
index 7d7d121fc9c..740101845b7 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_dynamic_size_pattern_with_unit.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_dynamic_size_pattern_with_unit.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 1830
 expression: "\npub fn go(x) {\n  let size = 3\n  case x {\n    <<1:size(size)-unit(2)>> -> 1\n    _ -> 2\n  }\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -21,7 +19,7 @@ import { bitArraySliceToInt } from "../gleam.mjs";
 export function go(x) {
   let size = 3;
   if (
-    size >= 0 &&
+    size * 2 >= 0 &&
     x.bitSize === size * 2 &&
     bitArraySliceToInt(x, 0, size * 2, true, false) === 1
   ) {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_match_dynamic_bytes_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_match_dynamic_bytes_size.snap
index 35041dced2f..7bcd0772ebe 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_match_dynamic_bytes_size.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__case_match_dynamic_bytes_size.snap
@@ -1,8 +1,6 @@
 ---
 source: compiler-core/src/javascript/tests/bit_arrays.rs
-assertion_line: 1090
 expression: "\npub fn go(x) {\n  let n = 3\n  case x {\n    <<a:bytes-size(n)>> -> a\n    _ -> x\n  }\n}\n"
-snapshot_kind: text
 ---
 ----- SOURCE CODE
 
@@ -20,7 +18,7 @@ import { bitArraySlice } from "../gleam.mjs";
 
 export function go(x) {
   let n = 3;
-  if (n >= 0 && x.bitSize === n * 8) {
+  if (n * 8 >= 0 && x.bitSize === n * 8) {
     let a = bitArraySlice(x, 0, n * 8);
     return a;
   } else {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__dynamic_size_pattern_with_unit.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__dynamic_size_pattern_with_unit.snap
index f8e47491036..703012b31ed 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__dynamic_size_pattern_with_unit.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__dynamic_size_pattern_with_unit.snap
@@ -18,7 +18,7 @@ const FILEPATH = "src/module.gleam";
 export function go(x) {
   let size = 3;
   if (!(
-    size >= 0 &&
+    size * 2 >= 0 &&
     x.bitSize === size * 2 &&
     bitArraySliceToInt(x, 0, size * 2, true, false) === 1
   )) {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bytes_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bytes_size.snap
index bd3a63f9e8a..8c4013bc350 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bytes_size.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__match_dynamic_bytes_size.snap
@@ -18,7 +18,7 @@ const FILEPATH = "src/module.gleam";
 export function go(x) {
   let n = 3;
   let a;
-  if (n >= 0 && x.bitSize === n * 8) {
+  if (n * 8 >= 0 && x.bitSize === n * 8) {
     a = bitArraySlice(x, 0, n * 8);
   } else {
     throw makeError(
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__non_byte_aligned_size_calculation.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__non_byte_aligned_size_calculation.snap
new file mode 100644
index 00000000000..c5124cf0fdf
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__non_byte_aligned_size_calculation.snap
@@ -0,0 +1,33 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  case <<>> {\n    <<a:1, b:3, c:size(b - 2)>> -> c + b\n    _ -> 1\n  }\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  case <<>> {
+    <<a:1, b:3, c:size(b - 2)>> -> c + b
+    _ -> 1
+  }
+}
+
+
+----- COMPILED JAVASCRIPT
+import { toBitArray, bitArraySliceToInt } from "../gleam.mjs";
+
+export function main() {
+  let $ = toBitArray([]);
+  if ($.bitSize >= 1 && $.bitSize >= 4) {
+    let b = bitArraySliceToInt($, 1, 4, true, false);
+    if (b - 2 >= 0 && $.bitSize === 4 + b - 2) {
+      let a = bitArraySliceToInt($, 0, 1, true, false);
+      let b$1 = b;
+      let c = bitArraySliceToInt($, 4, 4 + b$1 - 2, true, false);
+      return c + b$1;
+    } else {
+      return 1;
+    }
+  } else {
+    return 1;
+  }
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size.snap
new file mode 100644
index 00000000000..8e7b1b002d8
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size.snap
@@ -0,0 +1,49 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let assert <<len, payload:size(len * 8)>> = <<>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert <<len, payload:size(len * 8)>> = <<>>
+}
+
+
+----- COMPILED JAVASCRIPT
+import { makeError, toBitArray, bitArraySliceToInt } from "../gleam.mjs";
+
+const FILEPATH = "src/module.gleam";
+
+export function main() {
+  let $ = toBitArray([]);
+  let len$1;
+  let payload;
+  if ($.bitSize >= 8) {
+    let len = $.byteAt(0);
+    if ($.bitSize === 8 + len * 8) {
+      len$1 = len;
+      payload = bitArraySliceToInt($, 8, 8 + len$1 * 8, true, false);
+    } else {
+      throw makeError(
+        "let_assert",
+        FILEPATH,
+        "my/mod",
+        3,
+        "main",
+        "Pattern match failed, no pattern matched the value.",
+        { value: $, start: 19, end: 67, pattern_start: 30, pattern_end: 60 }
+      )
+    }
+  } else {
+    throw makeError(
+      "let_assert",
+      FILEPATH,
+      "my/mod",
+      3,
+      "main",
+      "Pattern match failed, no pattern matched the value.",
+      { value: $, start: 19, end: 67, pattern_start: 30, pattern_end: 60 }
+    )
+  }
+  return $;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size2.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size2.snap
new file mode 100644
index 00000000000..8d701125b45
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size2.snap
@@ -0,0 +1,52 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let assert <<len, payload:size(len / 8 - 1)>> = <<>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert <<len, payload:size(len / 8 - 1)>> = <<>>
+}
+
+
+----- COMPILED JAVASCRIPT
+import { makeError, divideInt, toBitArray, bitArraySliceToInt } from "../gleam.mjs";
+
+const FILEPATH = "src/module.gleam";
+
+export function main() {
+  let $ = toBitArray([]);
+  let len$1;
+  let payload;
+  if ($.bitSize >= 8) {
+    let len = $.byteAt(0);
+    if (
+      (divideInt(len, 8)) - 1 >= 0 &&
+      $.bitSize === 8 + (divideInt(len, 8)) - 1
+    ) {
+      len$1 = len;
+      payload = bitArraySliceToInt($, 8, 8 + (divideInt(len$1, 8)) - 1, true, false);
+    } else {
+      throw makeError(
+        "let_assert",
+        FILEPATH,
+        "my/mod",
+        3,
+        "main",
+        "Pattern match failed, no pattern matched the value.",
+        { value: $, start: 19, end: 71, pattern_start: 30, pattern_end: 64 }
+      )
+    }
+  } else {
+    throw makeError(
+      "let_assert",
+      FILEPATH,
+      "my/mod",
+      3,
+      "main",
+      "Pattern match failed, no pattern matched the value.",
+      { value: $, start: 19, end: 71, pattern_start: 30, pattern_end: 64 }
+    )
+  }
+  return $;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size3.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size3.snap
new file mode 100644
index 00000000000..273bff2f634
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__operator_in_pattern_size3.snap
@@ -0,0 +1,51 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let additional = 10\n  let assert <<len, payload:size(len + additional * 8)>> = <<>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let additional = 10
+  let assert <<len, payload:size(len + additional * 8)>> = <<>>
+}
+
+
+----- COMPILED JAVASCRIPT
+import { makeError, toBitArray, bitArraySliceToInt } from "../gleam.mjs";
+
+const FILEPATH = "src/module.gleam";
+
+export function main() {
+  let additional = 10;
+  let $ = toBitArray([]);
+  let len$1;
+  let payload;
+  if ($.bitSize >= 8) {
+    let len = $.byteAt(0);
+    if (len + (additional * 8) >= 0 && $.bitSize === 8 + len + additional * 8) {
+      len$1 = len;
+      payload = bitArraySliceToInt($, 8, 8 + len$1 + (additional * 8), true, false);
+    } else {
+      throw makeError(
+        "let_assert",
+        FILEPATH,
+        "my/mod",
+        4,
+        "main",
+        "Pattern match failed, no pattern matched the value.",
+        { value: $, start: 41, end: 102, pattern_start: 52, pattern_end: 95 }
+      )
+    }
+  } else {
+    throw makeError(
+      "let_assert",
+      FILEPATH,
+      "my/mod",
+      4,
+      "main",
+      "Pattern match failed, no pattern matched the value.",
+      { value: $, start: 41, end: 102, pattern_start: 52, pattern_end: 95 }
+    )
+  }
+  return $;
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__pattern_match_on_negative_size_calculation.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__pattern_match_on_negative_size_calculation.snap
new file mode 100644
index 00000000000..04b0f20ef92
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__bit_arrays__pattern_match_on_negative_size_calculation.snap
@@ -0,0 +1,64 @@
+---
+source: compiler-core/src/javascript/tests/bit_arrays.rs
+expression: "\npub fn main() {\n  let assert <<a, b:size(a - 100000), c:size(b)>> = <<1, 2, 3, 4, 5>>\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert <<a, b:size(a - 100000), c:size(b)>> = <<1, 2, 3, 4, 5>>
+}
+
+
+----- COMPILED JAVASCRIPT
+import { makeError, toBitArray, bitArraySliceToInt } from "../gleam.mjs";
+
+const FILEPATH = "src/module.gleam";
+
+export function main() {
+  let $ = toBitArray([1, 2, 3, 4, 5]);
+  let a$1;
+  let b$1;
+  let c;
+  if ($.bitSize >= 8) {
+    let a = $.byteAt(0);
+    if (a - 100000 >= 0) {
+      let b = bitArraySliceToInt($, 8, 8 + a - 100000, true, false);
+      if ($.bitSize === 8 + b + a - 100000) {
+        a$1 = a;
+        b$1 = b;
+        c = bitArraySliceToInt($, 8 + a - 100000, 8 + a - 100000 + b$1, true, false);
+      } else {
+        throw makeError(
+          "let_assert",
+          FILEPATH,
+          "my/mod",
+          3,
+          "main",
+          "Pattern match failed, no pattern matched the value.",
+          { value: $, start: 19, end: 86, pattern_start: 30, pattern_end: 66 }
+        )
+      }
+    } else {
+      throw makeError(
+        "let_assert",
+        FILEPATH,
+        "my/mod",
+        3,
+        "main",
+        "Pattern match failed, no pattern matched the value.",
+        { value: $, start: 19, end: 86, pattern_start: 30, pattern_end: 66 }
+      )
+    }
+  } else {
+    throw makeError(
+      "let_assert",
+      FILEPATH,
+      "my/mod",
+      3,
+      "main",
+      "Pattern match failed, no pattern matched the value.",
+      { value: $, start: 19, end: 86, pattern_start: 30, pattern_end: 66 }
+    )
+  }
+  return $;
+}
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__correct_precedence_in_pattern_size.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__correct_precedence_in_pattern_size.snap
new file mode 100644
index 00000000000..2fa5e260c8c
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__correct_precedence_in_pattern_size.snap
@@ -0,0 +1,135 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: "let assert <<size, payload:size(size + 2 * 8)>> = <<>>"
+---
+[
+    Assignment(
+        Assignment {
+            location: SrcSpan {
+                start: 0,
+                end: 54,
+            },
+            value: BitArray {
+                location: SrcSpan {
+                    start: 50,
+                    end: 54,
+                },
+                segments: [],
+            },
+            pattern: BitArray {
+                location: SrcSpan {
+                    start: 11,
+                    end: 47,
+                },
+                segments: [
+                    BitArraySegment {
+                        location: SrcSpan {
+                            start: 13,
+                            end: 17,
+                        },
+                        value: Variable {
+                            location: SrcSpan {
+                                start: 13,
+                                end: 17,
+                            },
+                            name: "size",
+                            type_: (),
+                            origin: VariableOrigin {
+                                syntax: Variable(
+                                    "size",
+                                ),
+                                declaration: LetPattern,
+                            },
+                        },
+                        options: [],
+                        type_: (),
+                    },
+                    BitArraySegment {
+                        location: SrcSpan {
+                            start: 19,
+                            end: 45,
+                        },
+                        value: Variable {
+                            location: SrcSpan {
+                                start: 19,
+                                end: 26,
+                            },
+                            name: "payload",
+                            type_: (),
+                            origin: VariableOrigin {
+                                syntax: Variable(
+                                    "payload",
+                                ),
+                                declaration: LetPattern,
+                            },
+                        },
+                        options: [
+                            Size {
+                                location: SrcSpan {
+                                    start: 27,
+                                    end: 45,
+                                },
+                                value: BitArraySize(
+                                    BinaryOperator {
+                                        location: SrcSpan {
+                                            start: 32,
+                                            end: 44,
+                                        },
+                                        operator: Add,
+                                        left: Variable {
+                                            location: SrcSpan {
+                                                start: 32,
+                                                end: 36,
+                                            },
+                                            name: "size",
+                                            constructor: None,
+                                            type_: (),
+                                        },
+                                        right: BinaryOperator {
+                                            location: SrcSpan {
+                                                start: 39,
+                                                end: 44,
+                                            },
+                                            operator: Multiply,
+                                            left: Int {
+                                                location: SrcSpan {
+                                                    start: 39,
+                                                    end: 40,
+                                                },
+                                                value: "2",
+                                                int_value: 2,
+                                            },
+                                            right: Int {
+                                                location: SrcSpan {
+                                                    start: 43,
+                                                    end: 44,
+                                                },
+                                                value: "8",
+                                                int_value: 8,
+                                            },
+                                        },
+                                    },
+                                ),
+                                short_form: false,
+                            },
+                        ],
+                        type_: (),
+                    },
+                ],
+            },
+            kind: Assert {
+                location: SrcSpan {
+                    start: 0,
+                    end: 10,
+                },
+                assert_keyword_start: 4,
+                message: None,
+            },
+            compiled_case: CompiledCase {
+                tree: Fail,
+                subject_variables: [],
+            },
+            annotation: None,
+        },
+    ),
+]
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__operator_in_pattern_size.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__operator_in_pattern_size.snap
new file mode 100644
index 00000000000..19c9698c934
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__operator_in_pattern_size.snap
@@ -0,0 +1,120 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: "let assert <<size, payload:size(size - 1)>> = <<>>"
+---
+[
+    Assignment(
+        Assignment {
+            location: SrcSpan {
+                start: 0,
+                end: 50,
+            },
+            value: BitArray {
+                location: SrcSpan {
+                    start: 46,
+                    end: 50,
+                },
+                segments: [],
+            },
+            pattern: BitArray {
+                location: SrcSpan {
+                    start: 11,
+                    end: 43,
+                },
+                segments: [
+                    BitArraySegment {
+                        location: SrcSpan {
+                            start: 13,
+                            end: 17,
+                        },
+                        value: Variable {
+                            location: SrcSpan {
+                                start: 13,
+                                end: 17,
+                            },
+                            name: "size",
+                            type_: (),
+                            origin: VariableOrigin {
+                                syntax: Variable(
+                                    "size",
+                                ),
+                                declaration: LetPattern,
+                            },
+                        },
+                        options: [],
+                        type_: (),
+                    },
+                    BitArraySegment {
+                        location: SrcSpan {
+                            start: 19,
+                            end: 41,
+                        },
+                        value: Variable {
+                            location: SrcSpan {
+                                start: 19,
+                                end: 26,
+                            },
+                            name: "payload",
+                            type_: (),
+                            origin: VariableOrigin {
+                                syntax: Variable(
+                                    "payload",
+                                ),
+                                declaration: LetPattern,
+                            },
+                        },
+                        options: [
+                            Size {
+                                location: SrcSpan {
+                                    start: 27,
+                                    end: 41,
+                                },
+                                value: BitArraySize(
+                                    BinaryOperator {
+                                        location: SrcSpan {
+                                            start: 32,
+                                            end: 40,
+                                        },
+                                        operator: Subtract,
+                                        left: Variable {
+                                            location: SrcSpan {
+                                                start: 32,
+                                                end: 36,
+                                            },
+                                            name: "size",
+                                            constructor: None,
+                                            type_: (),
+                                        },
+                                        right: Int {
+                                            location: SrcSpan {
+                                                start: 39,
+                                                end: 40,
+                                            },
+                                            value: "1",
+                                            int_value: 1,
+                                        },
+                                    },
+                                ),
+                                short_form: false,
+                            },
+                        ],
+                        type_: (),
+                    },
+                ],
+            },
+            kind: Assert {
+                location: SrcSpan {
+                    start: 0,
+                    end: 10,
+                },
+                assert_keyword_start: 4,
+                message: None,
+            },
+            compiled_case: CompiledCase {
+                tree: Fail,
+                subject_variables: [],
+            },
+            annotation: None,
+        },
+    ),
+]
diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs
index 00e91fcffcf..321900b7cd5 100644
--- a/compiler-core/src/parse/tests.rs
+++ b/compiler-core/src/parse/tests.rs
@@ -1827,3 +1827,13 @@ fn special_error_for_pythonic_neste_import() {
 fn doesnt_issue_special_error_for_pythonic_import_if_slash() {
     assert_module_error!("import one/two.three");
 }
+
+#[test]
+fn operator_in_pattern_size() {
+    assert_parse!("let assert <<size, payload:size(size - 1)>> = <<>>");
+}
+
+#[test]
+fn correct_precedence_in_pattern_size() {
+    assert_parse!("let assert <<size, payload:size(size + 2 * 8)>> = <<>>");
+}
diff --git a/test/language/test/language_test.gleam b/test/language/test/language_test.gleam
index fe3e8495339..909ac61edcf 100644
--- a/test/language/test/language_test.gleam
+++ b/test/language/test/language_test.gleam
@@ -1212,6 +1212,52 @@ fn sized_bit_array_tests() -> List(Test) {
         let size = 5
         assert_equal(True, <<405:size(size)-unit(2)>> == <<101, 1:2>>)
       }),
+    "let assert <<len, payload:bits-size(len * 8 - 4)>>"
+      |> example(fn() {
+        assert_equal(<<1, 2, 3, 4:4>>, {
+          let assert <<len, payload:bits-size(len * 8 - 4)>> = <<
+            4, 1, 2, 3, 4:4,
+          >>
+          payload
+        })
+      }),
+    "let assert <<len, payload:bytes-size(len / 8 + 2)>>"
+      |> example(fn() {
+        assert_equal(<<1, 2, 3, 4, 5, 6>>, {
+          let assert <<len, payload:bytes-size(len / 8 + 2)>> = <<
+            32, 1, 2, 3, 4, 5, 6,
+          >>
+          payload
+        })
+      }),
+    "let additional = 5\nlet assert <<len, payload:bits-size(len + additional * 8)>>"
+      |> example(fn() {
+        assert_equal(<<1, 2, 3, 4, 5, 6>>, {
+          let additional = 5
+          let assert <<len, payload:bits-size(len + additional * 8)>> = <<
+            8, 1, 2, 3, 4, 5, 6,
+          >>
+          payload
+        })
+      }),
+    "let assert <<len, payload:bits-size({ len + 1 } * 8)>>"
+      |> example(fn() {
+        assert_equal(<<1, 2, 3, 4>>, {
+          let assert <<len, payload:bits-size({ len + 1 } * 8)>> = <<
+            3, 1, 2, 3, 4,
+          >>
+          payload
+        })
+      }),
+    "Pattern match on negative size"
+      |> example(fn() {
+        assert_equal(2, {
+          case <<1, 2, 3, 4>> {
+            <<a, b:size(a - 100_000), _c:size(b)>> -> 1
+            _ -> 2
+          }
+        })
+      }),
   ]
 }