diff --git a/Cargo.lock b/Cargo.lock
index e60da52a51b868f11e416491c6920b7769db653e..ba4de2d5cebcc6d51435aed97cb43ef21737883a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,21 @@ version = "1.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
 
+[[package]]
+name = "bitflags"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+
+[[package]]
+name = "borsh"
+version = "1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
+dependencies = [
+ "cfg_aliases",
+]
+
 [[package]]
 name = "bytecount"
 version = "0.6.8"
@@ -30,25 +45,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
-name = "codespan"
-version = "0.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e4b418d52c9206820a56fc1aa28db73d67e346ba8ba6aa90987e8d6becef7e4"
-dependencies = [
- "codespan-reporting",
- "serde",
-]
-
-[[package]]
-name = "codespan-reporting"
-version = "0.12.0"
+name = "cfg_aliases"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
-dependencies = [
- "serde",
- "termcolor",
- "unicode-width",
-]
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 
 [[package]]
 name = "convert_case"
@@ -77,16 +77,10 @@ dependencies = [
  "convert_case",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn",
  "unicode-xid",
 ]
 
-[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-
 [[package]]
 name = "gdextension-api"
 version = "0.2.2"
@@ -175,57 +169,25 @@ name = "grimoire"
 version = "0.0.1"
 dependencies = [
  "anyhow",
- "godot",
- "grimoire-parser",
- "thiserror",
-]
-
-[[package]]
-name = "grimoire-parser"
-version = "0.0.1"
-dependencies = [
+ "bitflags",
  "bytecount",
  "cfg-if",
- "codespan",
- "codespan-reporting",
  "derive_more",
- "grimoire-parser-derive",
+ "godot",
  "log",
- "paste",
-]
-
-[[package]]
-name = "grimoire-parser-derive"
-version = "0.0.1"
-dependencies = [
- "indexmap",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
+ "memchr",
+ "nom",
+ "nom_locate",
+ "smol_str",
+ "thiserror",
 ]
 
-[[package]]
-name = "hashbrown"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
-
 [[package]]
 name = "heck"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
-[[package]]
-name = "indexmap"
-version = "2.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
-dependencies = [
- "equivalent",
- "hashbrown",
-]
-
 [[package]]
 name = "libc"
 version = "0.2.172"
@@ -278,10 +240,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a846cbc04412cf509efcd8f3694b114fc700a035fb5a37f21517f9fb019f1ebc"
 
 [[package]]
-name = "paste"
-version = "1.0.15"
+name = "nom"
+version = "8.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "nom_locate"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
+dependencies = [
+ "bytecount",
+ "memchr",
+ "nom",
+]
 
 [[package]]
 name = "proc-macro2"
@@ -347,18 +323,17 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn",
 ]
 
 [[package]]
-name = "syn"
-version = "1.0.109"
+name = "smol_str"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
 dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
+ "borsh",
+ "serde",
 ]
 
 [[package]]
@@ -372,15 +347,6 @@ dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "termcolor"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
-dependencies = [
- "winapi-util",
-]
-
 [[package]]
 name = "thiserror"
 version = "2.0.12"
@@ -398,7 +364,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn",
 ]
 
 [[package]]
@@ -419,12 +385,6 @@ version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
 
-[[package]]
-name = "unicode-width"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
-
 [[package]]
 name = "unicode-xid"
 version = "0.2.6"
@@ -440,85 +400,3 @@ dependencies = [
  "proc-macro2",
  "quote",
 ]
-
-[[package]]
-name = "winapi-util"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
-dependencies = [
- "windows-sys",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/Cargo.toml b/Cargo.toml
index 084e1728f0ce34c82c2ebb04add38701bba2ffbe..3edf4d3efab4f40fb91fd6356dc3f01559bf6d40 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,33 +1,20 @@
-[workspace]
-resolver = "2"
-members = ["grimoire-*"]
-package = { version = "0.0.1", license = "MIT", rust-version = "1.86", edition = "2024", authors = [
-    "Maëlle Martin <maelle.martin@proton.me>",
-] }
-
-
 [package]
-name        = "grimoire"
-description = "The Grimoire Engine, a VN engine done for fun"
-
-version.workspace      = true
-edition.workspace      = true
-license.workspace      = true
-authors.workspace      = true
-rust-version.workspace = true
+name         = "grimoire"
+description  = "The Grimoire Engine, a VN engine done for fun"
+version      = "0.0.1"
+license      = "MIT"
+rust-version = "1.86"
+edition      = "2024"
+
+authors = [
+    "Maëlle Martin <maelle.martin@proton.me>",
+]
 
 
 [lib]
 crate-type = ["cdylib"]
 
 
-[dependencies]
-grimoire-parser.workspace = true
-thiserror.workspace = true
-anyhow = "1"
-godot = { git = "https://github.com/godot-rust/gdext.git", features = ["api-4-4", "register-docs"] }
-
-
 [profile.release]
 debug         = false
 opt-level     = 3
@@ -42,10 +29,10 @@ debug     = true
 opt-level = "s"
 
 
-[workspace.dependencies]
-grimoire-parser        = { path = "grimoire-parser" }
-grimoire-parser-derive = { path = "grimoire-parser-derive" }
-
+[dependencies]
+anyhow      = "1"
+nom         = "8"
+nom_locate  = "5"
 bytecount   = "0.6"
 cfg-if      = "1"
 thiserror   = "2"
@@ -53,3 +40,8 @@ log         = "0.4"
 derive_more = { version = "2",   features = ["full"] }
 smol_str    = { version = "0.3", features = ["serde"] }
 bitflags    = { version = "2",   default-features = false }
+memchr      = { version = "*",   default-features = false } # ^1.0.0 + ^2.0
+
+[dependencies.godot]
+git      = "https://github.com/godot-rust/gdext.git"
+features = ["api-4-4", "register-docs"]
diff --git a/grimoire-parser-derive/Cargo.toml b/grimoire-parser-derive/Cargo.toml
deleted file mode 100644
index 4da1313d511822de34508ee85c9d2c0f295fec07..0000000000000000000000000000000000000000
--- a/grimoire-parser-derive/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[package]
-name        = "grimoire-parser-derive"
-description = "Internally used for the grimoire-parser crate."
-
-
-license.workspace = true
-version.workspace = true
-edition.workspace = true
-authors.workspace = true
-
-
-[lib]
-proc-macro = true
-
-
-[dependencies]
-syn         = { version = "1", features = ["extra-traits"] }
-quote       = "1"
-proc-macro2 = "1"
-indexmap    = "2"
diff --git a/grimoire-parser-derive/src/lib.rs b/grimoire-parser-derive/src/lib.rs
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/grimoire-parser/Cargo.toml b/grimoire-parser/Cargo.toml
deleted file mode 100644
index 6ff9dcac36dbb783dc3f041c8ba24ee9cf71fbc6..0000000000000000000000000000000000000000
--- a/grimoire-parser/Cargo.toml
+++ /dev/null
@@ -1,20 +0,0 @@
-[package]
-name        = "grimoire-parser"
-description = "A parser for Grimoire Scripts."
-
-license.workspace = true
-version.workspace = true
-edition.workspace = true
-authors.workspace = true
-
-[dependencies]
-grimoire-parser-derive.workspace = true
-
-bytecount.workspace   = true
-cfg-if.workspace      = true
-derive_more.workspace = true
-log.workspace         = true
-
-paste              = "1"
-codespan           = "0.12"
-codespan-reporting = "0.12"
diff --git a/grimoire-parser/src/lib.rs b/grimoire-parser/src/lib.rs
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/src/expr/mod.rs b/src/expr/mod.rs
index 29a991e560a564b65f40f5b8d1096ff9b7619a70..fa12c7701d34e8d5ff0cf3c289b17f43b8642f83 100644
--- a/src/expr/mod.rs
+++ b/src/expr/mod.rs
@@ -1,2 +1,86 @@
 #[derive(Debug)]
-pub enum Expression {}
+pub enum VarOrConstant {
+    Var(String),
+    Constant(Constant),
+}
+
+#[derive(Debug)]
+pub enum Constant {
+    Int(i32),
+    Flt(f32),
+    Bool(bool),
+    String(String),
+}
+
+#[derive(Debug)]
+pub enum Expression {
+    Binary(Box<Expression>, BinOp, Box<Expression>),
+    Unary(UnOp, Box<Expression>),
+    Leaf(VarOrConstant),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum UnOp {
+    Not,
+    Neg,
+}
+
+impl UnOp {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            UnOp::Not => todo!(),
+            UnOp::Neg => todo!(),
+        }
+    }
+}
+
+impl AsRef<str> for UnOp {
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum BinOp {
+    Add,
+    Sub,
+    Mul,
+    Div,
+    Mod,
+    CompGE,
+    CompGT,
+    CompLT,
+    CompLE,
+    Or,
+    And,
+    Xor,
+    CompEQ,
+    CompNEQ,
+}
+
+impl BinOp {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            BinOp::Add => todo!(),
+            BinOp::Sub => todo!(),
+            BinOp::Mul => todo!(),
+            BinOp::Div => todo!(),
+            BinOp::Mod => todo!(),
+            BinOp::CompGE => todo!(),
+            BinOp::CompGT => todo!(),
+            BinOp::CompLT => todo!(),
+            BinOp::CompLE => todo!(),
+            BinOp::Or => todo!(),
+            BinOp::And => todo!(),
+            BinOp::Xor => todo!(),
+            BinOp::CompEQ => todo!(),
+            BinOp::CompNEQ => todo!(),
+        }
+    }
+}
+
+impl AsRef<str> for BinOp {
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 61bc0a7316e81b0237aae3e0d9c8b0e8bc04d9c3..56217569cdc5ef6d03c7964cbda13df30a5460c1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,11 +1,12 @@
-pub mod spell;
+pub mod error;
 pub mod expr;
+pub mod parser;
+pub mod spell;
 pub mod state;
-pub mod error;
 
 pub mod dummy;
 
-use godot::prelude::*;
+use godot::prelude::{ExtensionLibrary, gdextension};
 
 struct GrimoireExtension;
 
diff --git a/src/parser/error.rs b/src/parser/error.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6c129bcce1f571c12b06517abf15206a8336acea
--- /dev/null
+++ b/src/parser/error.rs
@@ -0,0 +1,45 @@
+use crate::parser::span::{Location, Span};
+use nom::{IResult, error::*};
+use std::num::{ParseFloatError, ParseIntError};
+
+#[derive(Debug, thiserror::Error)]
+pub enum ParserError {
+    #[error("{0:?}, {1}")]
+    ParseFloat(Location, ParseFloatError),
+
+    #[error("{0:?}, {1}")]
+    ParseInt(Location, ParseIntError),
+
+    #[error("{0:?}, {1:?}")]
+    Nom(Location, ErrorKind),
+}
+
+pub type ParserResult<'a, T> = IResult<Span<'a>, T, ParserError>;
+
+impl<I> ContextError<I> for ParserError {
+    fn add_context(_: I, _ctx: &'static str, other: Self) -> Self {
+        other
+    }
+}
+
+impl ParseError<Span<'_>> for ParserError {
+    fn from_error_kind(input: Span<'_>, kind: ErrorKind) -> Self {
+        Self::Nom(input.get_location(), kind)
+    }
+
+    fn append(_: Span<'_>, _: ErrorKind, other: Self) -> Self {
+        other
+    }
+}
+
+impl nom::error::FromExternalError<Span<'_>, ParseFloatError> for ParserError {
+    fn from_external_error(input: Span<'_>, _: ErrorKind, e: ParseFloatError) -> Self {
+        Self::ParseFloat(input.get_location(), e)
+    }
+}
+
+impl nom::error::FromExternalError<Span<'_>, ParseIntError> for ParserError {
+    fn from_external_error(input: Span<'_>, _: ErrorKind, e: ParseIntError) -> Self {
+        Self::ParseInt(input.get_location(), e)
+    }
+}
diff --git a/src/parser/expr.rs b/src/parser/expr.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2f0b272801955f26e8c3674de085f5adfc94f2eb
--- /dev/null
+++ b/src/parser/expr.rs
@@ -0,0 +1,99 @@
+use crate::{
+    expr::{BinOp, Expression, UnOp},
+    parser::*,
+};
+
+use nom::{
+    Parser, branch::alt, bytes::complete::tag, character::complete::multispace0, combinator::map,
+    multi::many0, sequence::preceded,
+};
+
+fn leaf(s: Span) -> ParserResult<Expression> {
+    alt((
+        // 1
+        unop(UnOp::Not, expression),
+        unop(UnOp::Neg, expression),
+        // 2
+        utils::parse_paren(expression),
+    ))
+    .parse(s)
+}
+
+fn unop<'a>(
+    op: UnOp,
+    next: impl Parser<Span<'a>, Output = Expression, Error = ParserError>,
+) -> impl Parser<Span<'a>, Output = Expression, Error = ParserError> {
+    map((multispace0, tag(op.as_str()), next), move |(.., expr)| {
+        Expression::Unary(op, Box::new(expr))
+    })
+}
+
+fn binop<'a>(
+    op: BinOp,
+    next: impl Parser<Span<'a>, Output = Expression, Error = ParserError>,
+) -> impl Parser<Span<'a>, Output = (BinOp, Expression), Error = ParserError> {
+    preceded(multispace0, (map(tag(op.as_str()), move |_| op), next))
+}
+
+fn fold_exprs(initial: Expression, remainder: Vec<(BinOp, Expression)>) -> Expression {
+    remainder.into_iter().fold(initial, |acc, (op, expr)| {
+        Expression::Binary(Box::new(acc), op, Box::new(expr))
+    })
+}
+
+fn terms(s: Span) -> ParserResult<Expression> {
+    let (s, initial) = leaf(s)?;
+    let (s, remainder): (Span, Vec<(BinOp, Expression)>) = many0(alt((
+        binop(BinOp::Mul, leaf),
+        binop(BinOp::Div, leaf),
+        binop(BinOp::Mod, leaf),
+    )))
+    .parse(s)?;
+    Ok((s, fold_exprs(initial, remainder)))
+}
+
+fn sums(s: Span) -> ParserResult<Expression> {
+    let (s, initial) = terms(s)?;
+    let (s, remainder) =
+        many0(alt((binop(BinOp::Add, terms), binop(BinOp::Sub, terms)))).parse(s)?;
+    Ok((s, fold_exprs(initial, remainder)))
+}
+
+fn logic_terms(s: Span) -> ParserResult<Expression> {
+    let (s, initial) = sums(s)?;
+    let (s, remainder) = many0(binop(BinOp::And, sums)).parse(s)?;
+    Ok((s, fold_exprs(initial, remainder)))
+}
+
+fn logic_sums(s: Span) -> ParserResult<Expression> {
+    let (s, initial) = logic_terms(s)?;
+    let (s, remainder) = many0(alt((
+        binop(BinOp::Xor, logic_terms),
+        binop(BinOp::Or, logic_terms),
+    )))
+    .parse(s)?;
+    Ok((s, fold_exprs(initial, remainder)))
+}
+
+fn logic_comp(s: Span) -> ParserResult<Expression> {
+    let (s, initial) = logic_sums(s)?;
+    let (s, remainder) = many0(alt((
+        binop(BinOp::CompLE, logic_sums),
+        binop(BinOp::CompGE, logic_sums),
+        binop(BinOp::CompLT, logic_sums),
+        binop(BinOp::CompGT, logic_sums),
+    )))
+    .parse(s)?;
+    Ok((s, fold_exprs(initial, remainder)))
+}
+
+#[allow(dead_code)]
+pub(crate) fn expression(s: Span) -> ParserResult<Expression> {
+    let (s, initial) = logic_comp(s)?;
+    let (s, remainder) = many0(alt((
+        binop(BinOp::CompEQ, logic_comp),
+        binop(BinOp::CompNEQ, logic_comp),
+    )))
+    .parse(s)?;
+    Ok((s, fold_exprs(initial, remainder)))
+}
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fe3d0641b423e6c3f2c06dfa18e7f941a33211af
--- /dev/null
+++ b/src/parser/mod.rs
@@ -0,0 +1,7 @@
+mod error;
+mod expr;
+mod span;
+pub mod utils;
+
+pub use error::*;
+pub use span::*;
diff --git a/src/parser/span.rs b/src/parser/span.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6a0deb3820b578ddcbc4512607406ad275be10cd
--- /dev/null
+++ b/src/parser/span.rs
@@ -0,0 +1,142 @@
+use nom::{Compare, Input, Offset as _};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Location {
+    offset: usize,
+    line: u32,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Span<'a> {
+    /// The offset represents the position of the fragment relatively to
+    /// the input of the parser. It starts at offset 0.
+    offset: usize,
+
+    /// The line number of the fragment relatively to the input of the
+    /// parser. It starts at line 1.
+    line: u32,
+
+    /// The fragment that is spanned.
+    /// The fragment represents a part of the input of the parser.
+    fragment: &'a str,
+}
+
+impl<'a> core::ops::Deref for Span<'a> {
+    type Target = &'a str;
+    fn deref(&self) -> &Self::Target {
+        &self.fragment
+    }
+}
+
+impl<'a> Span<'a> {
+    pub fn new(fragment: &'a str) -> Span<'a> {
+        Self {
+            offset: 0,
+            line: 1,
+            fragment,
+        }
+    }
+
+    pub fn get_location(&self) -> Location {
+        todo!()
+    }
+
+    /// The offset represents the position of the fragment relatively to
+    /// the input of the parser. It starts at offset 0.
+    pub fn location_offset(&self) -> usize {
+        self.offset
+    }
+
+    /// The line number of the fragment relatively to the input of the
+    /// parser. It starts at line 1.
+    pub fn location_line(&self) -> u32 {
+        self.line
+    }
+
+    /// The fragment that is spanned.
+    /// The fragment represents a part of the input of the parser.
+    pub fn fragment(&self) -> &str {
+        &self.fragment
+    }
+
+    pub fn into_fragment(self) -> &'a str {
+        self.fragment
+    }
+
+    fn slice_by(&self, next_fragment: &'a str) -> Self {
+        let consumed_len = self.fragment.offset(&next_fragment);
+        if consumed_len == 0 {
+            return Self {
+                line: self.line,
+                offset: self.offset,
+                fragment: next_fragment,
+            };
+        }
+
+        let consumed = self.fragment.take(consumed_len);
+
+        let next_offset = self.offset + consumed_len;
+
+        let consumed_as_bytes = consumed.as_bytes();
+        let iter = memchr::Memchr::new(b'\n', consumed_as_bytes);
+        let number_of_lines = iter.count() as u32;
+        let next_line = self.line + number_of_lines;
+
+        Self {
+            line: next_line,
+            offset: next_offset,
+            fragment: next_fragment,
+        }
+    }
+}
+
+impl<'a> Input for Span<'a> {
+    type Item = <&'a str as Input>::Item;
+    type Iter = <&'a str as Input>::Iter;
+    type IterIndices = <&'a str as Input>::IterIndices;
+
+    fn input_len(&self) -> usize {
+        self.fragment.input_len()
+    }
+
+    fn take(&self, index: usize) -> Self {
+        self.slice_by(self.fragment.take(index))
+    }
+
+    fn take_from(&self, index: usize) -> Self {
+        self.slice_by(self.fragment.take_from(index))
+    }
+
+    fn take_split(&self, index: usize) -> (Self, Self) {
+        (self.take_from(index), self.take(index))
+    }
+
+    fn position<P>(&self, predicate: P) -> Option<usize>
+    where
+        P: Fn(Self::Item) -> bool,
+    {
+        self.fragment.position(predicate)
+    }
+
+    fn iter_elements(&self) -> Self::Iter {
+        self.fragment.iter_elements()
+    }
+
+    fn iter_indices(&self) -> Self::IterIndices {
+        self.fragment.iter_indices()
+    }
+
+    fn slice_index(&self, count: usize) -> Result<usize, nom::Needed> {
+        self.fragment.slice_index(count)
+    }
+}
+
+impl Compare<&str> for Span<'_> {
+    fn compare(&self, t: &str) -> nom::CompareResult {
+        todo!()
+    }
+
+    fn compare_no_case(&self, t: &str) -> nom::CompareResult {
+        todo!()
+    }
+}
diff --git a/src/parser/utils.rs b/src/parser/utils.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ca8516ddc4b9d7192e1738eb4f9050db8121c3aa
--- /dev/null
+++ b/src/parser/utils.rs
@@ -0,0 +1,13 @@
+use crate::parser::{error::*, span::*};
+use nom::{
+    Parser,
+    bytes::complete::tag,
+    character::complete::multispace0,
+    sequence::{delimited, preceded},
+};
+
+pub fn parse_paren<'a, T>(
+    inner: impl Parser<Span<'a>, Output = T, Error = ParserError>,
+) -> impl Parser<Span<'a>, Output = T, Error = ParserError> {
+    preceded(multispace0, delimited(tag("("), inner, tag(")")))
+}