diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,36 @@
 # Revision history for stim-parser
 
-## 0.1.0.0 -- YYYY-mm-dd
+## 0.2.0.0 -- 2026-04-01
 
-* First version. Released on an unsuspecting world.
+* Add DEM (Detector Error Model) parsing support.
+  New modules: `StimParser.DEM.Expr`, `StimParser.DEM.Parse`.
+* Add `flattenDEM` utility to expand `repeat` blocks and apply
+  `shift_detectors` coordinate/ID shifts.
+* Migrate all floating-point types from `Float` to `Double` for
+  higher precision probability representation.
+* Move `parseExhaust`, `parseTuple`, and `parseTupleFloat` to
+  `StimParser.ParseUtils` as shared lexer utilities.
+* Add `parseNumber` for parsing bare integers or floats as `Double`.
+* Expand test suite with DEM parser and flattening tests.
+
+## 0.1.0.0 -- 2026-03-31
+
+* Initial release.
+* Parse STIM quantum circuit files into a Haskell AST.
+* Supported circuit elements:
+    * Gates (Pauli, single-qubit Clifford, two-qubit Clifford,
+      collapsing gates)
+    * Measurements (M, MXX, MYY, MZZ, MRX, MRY, MRZ, MX, MY, MZ, MR)
+    * Generalized Pauli Product gates (MPP, SPP, SPP_DAG)
+    * Noise channels (DEPOLARIZE, PAULI_CHANNEL, X/Y/Z_ERROR,
+      CORRELATED_ERROR, etc.)
+    * Annotations (DETECTOR, OBSERVABLE_INCLUDE, QUBIT_COORDS,
+      SHIFT_COORDS, TICK, MPAD)
+    * REPEAT blocks
+    * Tags on instructions
+* `StimParser.Trans.flattenQ` transform to resolve `rec[]` references
+  into absolute qubit indices, with coordinate shift support.
+* Two example executables: `stim-parser-example`,
+  `stim-parser-unit-example`.
+* Test suite covering expression types, parsers, parse utilities,
+  and transformations.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,48 @@
+# stim-parser 
+
+parser combinator to parse `stim`'s `.stim` file into `AST`.
+
+## Usage
+stim-parser is expected to be used as a `haskell` library, for example:
+```haskell
+import StimParser.Expr
+import StimParser.Parse
+import StimParser.ParseUtils (run)
+
+-- Parse a stim gate
+main :: IO ()
+main = do
+  -- Parse individual elements
+  print $ run parseGate "CNOT 0 1 2 3"
+  -- => Gate CNOT [Q 0,Q 1,Q 2,Q 3]
+  
+  print $ run parseMeasure "MXX(0.01) 2 3"
+  -- => Measure MXX (Just 0.01) [Q 2,Q 3]
+  
+  -- Parse a full stim file
+  s <- readFile "example.stim"
+  print $ run parseStim ("!!!Start " ++ s)
+  -- => StimList [...]
+```
+
+See `data/example.stim` for a sample input file.
+
+## Executables & Tests
+
+### stim-parser-example
+Main example from the README usage section. Parses a sample stim file(`data/example.stim`):
+```bash
+cabal run stim-parser-example
+```
+
+### stim-parser-unit-example
+Multiple unit examples demonstrating individual parsers (gates, measurements, noise channels, annotations, etc.):
+```bash
+cabal run stim-parser-unit-example
+```
+
+### Test Suite
+Unit tests for all modules (Expr, ParseUtils, Parse, Trans):
+```bash
+cabal test
+```
diff --git a/src/StimParser/DEM/Expr.hs b/src/StimParser/DEM/Expr.hs
new file mode 100644
--- /dev/null
+++ b/src/StimParser/DEM/Expr.hs
@@ -0,0 +1,110 @@
+{-# LANGUAGE InstanceSigs #-}
+module StimParser.DEM.Expr where
+
+-- | A detector index (check node in the bipartite graph).
+newtype DetectorId = DetectorId { unDetectorId :: Int }
+  deriving (Show, Eq, Ord)
+
+-- | A logical observable index.
+newtype ObservableId = ObservableId { unObservableId :: Int }
+  deriving (Show, Eq, Ord)
+
+-- | Targets of an error instruction: detectors and/or observables.
+data DEMTarget
+    = TargetDetector !DetectorId
+    | TargetObservable !ObservableId
+    deriving (Show, Eq)
+
+-- | A single error instruction: error(p) D0 D1 L0
+--
+-- Probability is stored as raw Double (not LLR). Downstream packages
+-- compute LLRs if needed.
+data DEMError = DEMError
+    { deProbability :: !Double
+    , deTargets     :: ![DEMTarget]
+    } deriving (Show, Eq)
+
+-- | A detector declaration: detector(0, 0) D0
+--
+-- Coordinates are stored as [Double] to support arbitrary dimensionality.
+data DEMDetector = DEMDetector
+    { ddId     :: !DetectorId
+    , ddCoords :: ![Double]
+    } deriving (Show, Eq)
+
+-- | A logical observable declaration: logical_observable L0
+data DEMObservable = DEMObservable
+    { doId :: !ObservableId
+    } deriving (Show, Eq)
+
+-- | Shift detectors instruction: shift_detectors(1, 0) 0
+--
+-- First argument is coordinate shift, second is detector ID shift.
+data DEMShift = DEMShift
+    { dsCoordShift :: ![Double]
+    , dsDetShift   :: !Int
+    } deriving (Show, Eq)
+
+-- | A DEM instruction (top-level line).
+data DEMInstruction
+    = DEMInstrError      !DEMError
+    | DEMInstrDetector   !DEMDetector
+    | DEMInstrObservable !DEMObservable
+    | DEMInstrShift      !DEMShift
+    | DEMInstrRepeat     !Int ![DEMInstruction]
+    deriving (Show, Eq)
+
+-- | A complete Detector Error Model.
+newtype DEM = DEM { unDEM :: [DEMInstruction] }
+  deriving (Show, Eq)
+
+-- ---------------------------------------------------------------------------
+-- Flattening utilities
+-- ---------------------------------------------------------------------------
+
+-- | Flatten a DEM by expanding all repeat blocks and applying shift_detectors.
+--
+-- After flattening, the resulting DEM contains only:
+--   - 'DEMInstrError'
+--   - 'DEMInstrDetector'
+--   - 'DEMInstrObservable'
+--
+-- All 'DEMInstrRepeat' are expanded into their body repeated N times.
+-- All 'DEMInstrShift' are applied to subsequent instructions' coordinates
+-- and detector IDs.
+--
+-- This mirrors 'StimParser.Trans.flattenQ' for circuits.
+flattenDEM :: DEM -> DEM
+flattenDEM (DEM instrs) = DEM (flattenInstrs [] 0 instrs)
+
+-- | Flatten a list of instructions with accumulated coordinate shift
+-- and detector ID shift.
+flattenInstrs :: [Double] -> Int -> [DEMInstruction] -> [DEMInstruction]
+flattenInstrs _ _ [] = []
+flattenInstrs coordShift detShift (i : is) =
+  case i of
+    DEMInstrShift (DEMShift cs ds) ->
+      let newCoordShift = zipWith (+) (padCoords (length cs) coordShift) cs
+          newDetShift = detShift + ds
+      in flattenInstrs newCoordShift newDetShift is
+
+    DEMInstrRepeat n body ->
+      flattenInstrs coordShift detShift (concat (replicate n body) ++ is)
+
+    DEMInstrDetector (DEMDetector (DetectorId did) coords) ->
+      let shiftPadded = padCoords (length coords) coordShift
+          shiftedCoords = zipWith (+) shiftPadded coords
+          shiftedId = DetectorId (did + detShift)
+      in DEMInstrDetector (DEMDetector shiftedId shiftedCoords) : flattenInstrs coordShift detShift is
+
+    DEMInstrError err ->
+      DEMInstrError err : flattenInstrs coordShift detShift is
+
+    DEMInstrObservable obs ->
+      DEMInstrObservable obs : flattenInstrs coordShift detShift is
+
+-- | Pad a coordinate list to a given length with zeros.
+-- This handles cases where shift_detectors has fewer coordinates than
+-- the detectors being shifted.
+padCoords :: Int -> [Double] -> [Double]
+padCoords n cs = take n (cs ++ repeat 0)
diff --git a/src/StimParser/DEM/Parse.hs b/src/StimParser/DEM/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/StimParser/DEM/Parse.hs
@@ -0,0 +1,70 @@
+{-# OPTIONS_GHC -Wno-unused-do-bind #-}
+module StimParser.DEM.Parse where
+
+import Text.Megaparsec
+import Text.Megaparsec.Char
+
+import StimParser.ParseUtils
+import StimParser.DEM.Expr
+
+-- | Parse a complete DEM string.
+parseDEM :: Parser DEM
+parseDEM = DEM <$> many parseDEMInstruction
+
+-- | Parse a single DEM instruction.
+parseDEMInstruction :: Parser DEMInstruction
+parseDEMInstruction =
+      DEMInstrError      <$> parseDEMError
+  <|> DEMInstrDetector   <$> parseDEMDetector
+  <|> DEMInstrObservable <$> parseDEMObservable
+  <|> DEMInstrShift      <$> parseDEMShift
+  <|> uncurry DEMInstrRepeat <$> parseDEMRepeat
+
+-- | Parse an error instruction: error(0.01) D0 D1 L0
+parseDEMError :: Parser DEMError
+parseDEMError = do
+  lstring "error"
+  lstring "("
+  p <- parseNumber
+  lstring ")"
+  targets <- many parseDEMTarget
+  return $ DEMError p targets
+
+-- | Parse a single target: D0 or L0
+parseDEMTarget :: Parser DEMTarget
+parseDEMTarget =
+      TargetDetector   . DetectorId   <$> (lstring "D" *> parseInt)
+  <|> TargetObservable . ObservableId <$> (lstring "L" *> parseInt)
+
+-- | Parse a detector declaration: detector(0, 0) D0
+parseDEMDetector :: Parser DEMDetector
+parseDEMDetector = do
+  lstring "detector"
+  coords <- parseTupleNumber
+  did <- DetectorId <$> (lstring "D" *> parseInt)
+  return $ DEMDetector did coords
+
+-- | Parse a logical observable declaration: logical_observable L0
+parseDEMObservable :: Parser DEMObservable
+parseDEMObservable = do
+  lstring "logical_observable"
+  oid <- ObservableId <$> (lstring "L" *> parseInt)
+  return $ DEMObservable oid
+
+-- | Parse a shift_detectors instruction: shift_detectors(1, 0) 0
+parseDEMShift :: Parser DEMShift
+parseDEMShift = do
+  lstring "shift_detectors"
+  coords <- parseTupleNumber
+  shift <- parseInt
+  return $ DEMShift coords shift
+
+-- | Parse a repeat block: repeat 100 { ... }
+parseDEMRepeat :: Parser (Int, [DEMInstruction])
+parseDEMRepeat = do
+  lstring "repeat"
+  n <- parseInt
+  lstring "{"
+  instrs <- many parseDEMInstruction
+  lstring "}"
+  return (n, instrs)
diff --git a/src/StimParser/Expr.hs b/src/StimParser/Expr.hs
--- a/src/StimParser/Expr.hs
+++ b/src/StimParser/Expr.hs
@@ -2,7 +2,7 @@
 module StimParser.Expr where 
 
 type Ind = Int
-type Ph = Float
+type Ph = Double
 newtype Rec = Rec Int
   deriving (Show)
 newtype Sweep = Sweep Int
@@ -140,12 +140,12 @@
 errorTagTyList = [TWO_QUBIT_LEAKAGE_NOISE_FOR_AN_ADVANCED_SIMULATOR ..] 
 
 data ErrorTag = ErrorTag ErrorTagTy
-  | ErrorTagCoef ErrorTagTy Float 
+  | ErrorTagCoef ErrorTagTy Double
   deriving (Show)
 
 data Noise = 
   NoiseNormal NoiseTy (Maybe Tag) (Maybe ErrorTag) [Ph] [Q]
-  | NoiseE NoiseTy (Maybe Tag) Float [PauliInd]
+  | NoiseE NoiseTy (Maybe Tag) Double [PauliInd]
   deriving (Show)
 
 -- General Instruction Tag (v1.15+)
@@ -174,7 +174,7 @@
 --    TICK[100ns]
 
 -- seperate Integer and Float cases
-data FInd = In Ind | Fl Float
+data FInd = In Ind | Fl Double
   deriving (Show)
 
 instance Num FInd where
diff --git a/src/StimParser/Parse.hs b/src/StimParser/Parse.hs
--- a/src/StimParser/Parse.hs
+++ b/src/StimParser/Parse.hs
@@ -24,9 +24,6 @@
   i <- L.decimal
   return $ PauliInd p i
 
-parseExhaust :: Parser a -> Parser [a]
-parseExhaust parseElm = safeManyTill parseElm (notFollowedBy parseElm)
-
 parsePauliChain :: Parser PauliChain
 parsePauliChain = do 
   let
@@ -120,7 +117,7 @@
 parseMeasureTy :: Parser MeasureTy
 parseMeasureTy = parseEnum measureTyList
 
-parsePh :: Parser Float 
+parsePh :: Parser Double
 parsePh = do 
   lstring "("
   ph <- parseFloat
@@ -189,33 +186,6 @@
   -- the order is tricky
   try pm2 <|> pm1
 
-parseTuple :: Parser a -> Parser [a]
-parseTuple pm = do 
-  lstring "("
-  let 
-    parseE = do 
-      f <- pm
-      lstring ","
-      return f
-    parseTuple_ = (++) <$> parseExhaust parseE <*> ((: []) <$> pm)
-  phs <- parseTuple_
-  lstring ")"
-  return phs
-
-parseTupleFloat :: Parser [Float]
-parseTupleFloat = parseTuple parseFloat
-  -- lstring "("
-  -- let 
-  --   parseE = do 
-  --     f <- parseFloat
-  --     lstring ","
-  --     return f
-  --   parseTuple = (++) <$> parseExhaust parseE <*> ((: []) <$> parseFloat)
-  -- phs <- parseTuple
-  -- lstring ")"
-  -- return phs
-
-
 parseNoise :: Parser Noise
 parseNoise = do 
   let 
@@ -273,7 +243,7 @@
     pm1 = do 
       i <- parseInt
       return $ In i 
-    -- Fl Float
+    -- Fl Double
     pm2 = do
       f <- parseFloat
       return $ Fl f 
diff --git a/src/StimParser/ParseUtils.hs b/src/StimParser/ParseUtils.hs
--- a/src/StimParser/ParseUtils.hs
+++ b/src/StimParser/ParseUtils.hs
@@ -51,6 +51,9 @@
   where
     go = try ([] <$ end) <|> liftA2 (:) p go
 
+parseExhaust :: Parser a -> Parser [a]
+parseExhaust parseElm = safeManyTill parseElm (notFollowedBy parseElm)
+
 manyBetween :: Parser a -> Parser a -> Parser String
 manyBetween s e = s *> safeManyTill L.charLiteral e
 
@@ -64,8 +67,35 @@
 parseInt :: Parser Int
 parseInt = lexeme $ L.signed sc L.decimal
 
-parseFloat :: Parser Float 
+parseFloat :: Parser Double
 parseFloat = lexeme $ L.signed sc L.float
+
+-- | Parse a number that may be an integer or a float.
+-- This is useful for coordinates and other contexts where integers
+-- like @0@ or @1@ should be accepted as @0.0@ or @1.0@.
+parseNumber :: Parser Double
+parseNumber = lexeme $ L.signed sc (try L.float <|> fromIntegral <$> L.decimal)
+
+parseTuple :: Parser a -> Parser [a]
+parseTuple pm = do
+  lstring "("
+  let
+    parseE = do
+      f <- pm
+      lstring ","
+      return f
+    parseTuple_ = (++) <$> parseExhaust parseE <*> ((: []) <$> pm)
+  phs <- parseTuple_
+  lstring ")"
+  return phs
+
+parseTupleFloat :: Parser [Double]
+parseTupleFloat = parseTuple parseFloat
+
+-- | Parse a tuple of numbers (integers or floats).
+-- Used for DEM coordinates where bare integers like @(0, 0)@ are common.
+parseTupleNumber :: Parser [Double]
+parseTupleNumber = parseTuple parseNumber
 
 parseVar :: Parser String
 parseVar = do
diff --git a/stim-parser.cabal b/stim-parser.cabal
--- a/stim-parser.cabal
+++ b/stim-parser.cabal
@@ -20,7 +20,7 @@
 -- PVP summary:     +-+------- breaking API changes
 --                  | | +----- non-breaking API additions
 --                  | | | +--- code changes with no API change
-version:            0.1.0.0
+version:            0.2.0.0
 
 -- A short (one-line) description of the package.
 synopsis:           A parser combinator library for STIM quantum circuit files
@@ -48,7 +48,8 @@
 build-type:         Simple
 
 -- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
-extra-doc-files:    CHANGELOG.md
+extra-doc-files:    README.md
+                  , CHANGELOG.md
 
 -- Extra source files to be distributed with the package, such as examples, or a tutorial module.
 -- extra-source-files:
@@ -73,6 +74,8 @@
       , StimParser.Parse
       , StimParser.ParseUtils
       , StimParser.Trans
+      , StimParser.DEM.Expr
+      , StimParser.DEM.Parse
     
 
 executable stim-parser-unit-example
@@ -133,6 +136,8 @@
                     , Test.ParseUtils
                     , Test.Parse
                     , Test.Trans
+                    , Test.DEM.Expr
+                    , Test.DEM.Parse
     build-depends:    base                 >= 4.11.0.0 && < 4.22
                     , stim-parser
                     , HUnit              >= 1.6     && < 1.7
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -7,6 +7,8 @@
 import qualified Test.ParseUtils as ParseUtils
 import qualified Test.Parse as Parse
 import qualified Test.Trans as Trans
+import qualified Test.DEM.Expr as DEMExpr
+import qualified Test.DEM.Parse as DEMParse
 
 main :: IO ()
 main = do
@@ -21,4 +23,6 @@
   , ParseUtils.tests
   , Parse.tests
   , Trans.tests
+  , DEMExpr.tests
+  , DEMParse.tests
   ]
diff --git a/test/Test/DEM/Expr.hs b/test/Test/DEM/Expr.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/DEM/Expr.hs
@@ -0,0 +1,187 @@
+module Test.DEM.Expr where
+
+import Test.HUnit
+import StimParser.DEM.Expr
+
+-- Helper to compare using show
+tAssertShowEqual :: (Show a) => String -> a -> a -> Test
+tAssertShowEqual msg expected actual = TestCase $
+  assertEqual msg (show expected) (show actual)
+
+tests :: Test
+tests = TestList
+  [ testFlattenDEMSimple
+  , testFlattenDEMRepeat
+  , testFlattenDEMShift
+  , testFlattenDEMShiftAndRepeat
+  , testFlattenDEMDetIdShift
+  , testFlattenDEMDimensionMismatch
+  , testFlattenDEMIdempotent
+  ]
+
+-- | Flattening a DEM with no repeats or shifts is identity
+testFlattenDEMSimple :: Test
+testFlattenDEMSimple = TestList
+  [ "flatten simple error" ~:
+      let dem = DEM [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])]
+      in dem ~=? flattenDEM dem
+
+  , "flatten multiple instructions" ~:
+      let dem = DEM
+            [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])
+            , DEMInstrError (DEMError 0.02 [TargetDetector (DetectorId 1), TargetObservable (ObservableId 0)])
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])
+            , DEMInstrObservable (DEMObservable (ObservableId 0))
+            ]
+      in dem ~=? flattenDEM dem
+  ]
+
+-- | Repeat blocks are expanded
+testFlattenDEMRepeat :: Test
+testFlattenDEMRepeat = TestList
+  [ "flatten repeat 2" ~:
+      let input = DEM
+            [ DEMInstrRepeat 2
+                [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])
+                ]
+            ]
+          expected = DEM
+            [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])
+            , DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])
+            ]
+      in expected ~=? flattenDEM input
+
+  , "flatten repeat 0" ~:
+      let input = DEM [DEMInstrRepeat 0 [DEMInstrError (DEMError 0.01 [])]]
+      in DEM [] ~=? flattenDEM input
+
+  , "flatten nested repeat" ~:
+      let input = DEM
+            [ DEMInstrRepeat 2
+                [ DEMInstrRepeat 3
+                    [ DEMInstrError (DEMError 0.01 []) ]
+                ]
+            ]
+          expected = DEM (replicate 6 (DEMInstrError (DEMError 0.01 [])))
+      in expected ~=? flattenDEM input
+  ]
+
+-- | shift_detectors coordinates are applied to subsequent detectors
+testFlattenDEMShift :: Test
+testFlattenDEMShift = TestList
+  [ "flatten shift detectors" ~:
+      let input = DEM
+            [ DEMInstrShift (DEMShift [1.0, 0.0] 0)
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])
+            , DEMInstrDetector (DEMDetector (DetectorId 1) [2.0, 3.0])
+            ]
+          expected = DEM
+            [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0, 0.0])
+            , DEMInstrDetector (DEMDetector (DetectorId 1) [3.0, 3.0])
+            ]
+      in expected ~=? flattenDEM input
+
+  , "flatten cumulative shift" ~:
+      let input = DEM
+            [ DEMInstrShift (DEMShift [1.0] 0)
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0])
+            , DEMInstrShift (DEMShift [2.0] 0)
+            , DEMInstrDetector (DEMDetector (DetectorId 1) [0.0])
+            ]
+          expected = DEM
+            [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0])
+            , DEMInstrDetector (DEMDetector (DetectorId 1) [3.0])
+            ]
+      in expected ~=? flattenDEM input
+  ]
+
+-- | Combined shift and repeat
+testFlattenDEMShiftAndRepeat :: Test
+testFlattenDEMShiftAndRepeat = TestList
+  [ "flatten shift inside repeat" ~:
+      let input = DEM
+            [ DEMInstrRepeat 2
+                [ DEMInstrShift (DEMShift [1.0] 0)
+                , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0])
+                ]
+            ]
+          -- Note: shifts are cumulative across repeat iterations.
+          -- Iteration 1: shift=[1.0], detector gets [1.0]
+          -- Iteration 2: shift becomes [2.0], detector gets [2.0]
+          expected = DEM
+            [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0])
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [2.0])
+            ]
+      in expected ~=? flattenDEM input
+  ]
+
+-- | Detector ID shifting
+testFlattenDEMDetIdShift :: Test
+testFlattenDEMDetIdShift = TestList
+  [ "flatten detector id shift" ~:
+      let input = DEM
+            [ DEMInstrShift (DEMShift [0.0] 5)
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [1.0])
+            , DEMInstrDetector (DEMDetector (DetectorId 1) [2.0])
+            ]
+          expected = DEM
+            [ DEMInstrDetector (DEMDetector (DetectorId 5) [1.0])
+            , DEMInstrDetector (DEMDetector (DetectorId 6) [2.0])
+            ]
+      in expected ~=? flattenDEM input
+
+  , "flatten combined coord and id shift" ~:
+      let input = DEM
+            [ DEMInstrShift (DEMShift [1.0, 0.0] 10)
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])
+            ]
+          expected = DEM
+            [ DEMInstrDetector (DEMDetector (DetectorId 10) [1.0, 0.0])
+            ]
+      in expected ~=? flattenDEM input
+  ]
+
+-- | Dimension mismatch between shift and detector coordinates
+testFlattenDEMDimensionMismatch :: Test
+testFlattenDEMDimensionMismatch = TestList
+  [ "2D shift on 3D detector" ~:
+      let input = DEM
+            [ DEMInstrShift (DEMShift [1.0, 0.0] 0)
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0, 0.0])
+            ]
+          expected = DEM
+            [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0, 0.0, 0.0])
+            ]
+      in expected ~=? flattenDEM input
+
+  , "3D shift on 2D detector" ~:
+      let input = DEM
+            [ DEMInstrShift (DEMShift [1.0, 0.0, 0.0] 0)
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])
+            ]
+          expected = DEM
+            [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0, 0.0])
+            ]
+      in expected ~=? flattenDEM input
+  ]
+
+-- | Idempotency: flattening an already-flattened DEM is a no-op
+testFlattenDEMIdempotent :: Test
+testFlattenDEMIdempotent = TestList
+  [ "flatten is idempotent on simple" ~:
+      let dem = DEM
+            [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])
+            , DEMInstrDetector (DEMDetector (DetectorId 1) [1.0, 2.0])
+            , DEMInstrObservable (DEMObservable (ObservableId 0))
+            ]
+      in flattenDEM dem ~=? flattenDEM (flattenDEM dem)
+
+  , "flatten is idempotent on nested" ~:
+      let dem = DEM
+            [ DEMInstrRepeat 2
+                [ DEMInstrShift (DEMShift [1.0] 0)
+                , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0])
+                ]
+            ]
+      in flattenDEM dem ~=? flattenDEM (flattenDEM dem)
+  ]
diff --git a/test/Test/DEM/Parse.hs b/test/Test/DEM/Parse.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/DEM/Parse.hs
@@ -0,0 +1,267 @@
+module Test.DEM.Parse where
+
+import Test.HUnit
+
+import StimParser.DEM.Expr
+import StimParser.DEM.Parse
+import StimParser.ParseUtils (run)
+
+-- Helper to compare using show
+tAssertShowEqual :: (Show a) => String -> a -> a -> Test
+tAssertShowEqual msg expected actual = TestCase $
+  assertEqual msg (show expected) (show actual)
+
+tests :: Test
+tests = TestList
+  [ testParseDEMError
+  , testParseDEMDetector
+  , testParseDEMObservable
+  , testParseDEMShift
+  , testParseDEMRepeat
+  , testParseDEMTarget
+  , testParseDEMFull
+  , testParseDEMHighPrecision
+  , testParseDEMWhitespace
+  , testParseDEMEdgeCases
+  , testParseDEMComments
+  , testParseDEMScientificCoords
+  , testParseDEMMixedTargets
+  , testParseDEMEmpty
+  ]
+
+-- | Parse individual error instructions
+testParseDEMError :: Test
+testParseDEMError = TestList
+  [ "error single detector" ~:
+      DEMError 0.01 [TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(0.01) D0"
+
+  , "error multiple detectors" ~:
+      DEMError 0.005 [TargetDetector (DetectorId 0), TargetDetector (DetectorId 1)]
+      ~=? run parseDEMError "error(0.005) D0 D1"
+
+  , "error with observable" ~:
+      DEMError 0.003 [TargetDetector (DetectorId 0), TargetObservable (ObservableId 0)]
+      ~=? run parseDEMError "error(0.003) D0 L0"
+
+  , "error multiple observables" ~:
+      DEMError 0.1 [TargetObservable (ObservableId 0), TargetObservable (ObservableId 1)]
+      ~=? run parseDEMError "error(0.1) L0 L1"
+
+  , "error no targets" ~:
+      DEMError 0.01 []
+      ~=? run parseDEMError "error(0.01)"
+
+  , "error probability 0" ~:
+      DEMError 0.0 [TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(0) D0"
+
+  , "error probability 1" ~:
+      DEMError 1.0 [TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(1) D0"
+
+  , "error scientific notation" ~:
+      DEMError 1.5e-2 [TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(1.5e-2) D0"
+
+  , "error very small probability" ~:
+      DEMError 1e-15 [TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(1e-15) D0"
+  ]
+
+-- | Parse detector declarations
+testParseDEMDetector :: Test
+testParseDEMDetector = TestList
+  [ "detector 2D" ~:
+      DEMDetector (DetectorId 0) [0.0, 0.0]
+      ~=? run parseDEMDetector "detector(0, 0) D0"
+
+  , "detector 3D" ~:
+      DEMDetector (DetectorId 5) [1.0, 2.0, 0.0]
+      ~=? run parseDEMDetector "detector(1, 2, 0) D5"
+
+  , "detector negative coords" ~:
+      DEMDetector (DetectorId 1) [-1.0, 0.5]
+      ~=? run parseDEMDetector "detector(-1, 0.5) D1"
+
+  , "detector 1D" ~:
+      DEMDetector (DetectorId 3) [2.0]
+      ~=? run parseDEMDetector "detector(2) D3"
+  ]
+
+-- | Parse logical observable declarations
+testParseDEMObservable :: Test
+testParseDEMObservable = TestList
+  [ "observable L0" ~:
+      DEMObservable (ObservableId 0)
+      ~=? run parseDEMObservable "logical_observable L0"
+
+  , "observable L5" ~:
+      DEMObservable (ObservableId 5)
+      ~=? run parseDEMObservable "logical_observable L5"
+  ]
+
+-- | Parse shift_detectors
+testParseDEMShift :: Test
+testParseDEMShift = TestList
+  [ "shift 2D" ~:
+      DEMShift [1.0, 0.0] 0
+      ~=? run parseDEMShift "shift_detectors(1, 0) 0"
+
+  , "shift with det offset" ~:
+      DEMShift [0.0, 1.0] 10
+      ~=? run parseDEMShift "shift_detectors(0, 1) 10"
+  ]
+
+-- | Parse repeat blocks
+testParseDEMRepeat :: Test
+testParseDEMRepeat = TestList
+  [ "repeat simple" ~:
+      (2, [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])])
+      ~=? run parseDEMRepeat "repeat 2 { error(0.01) D0 }"
+
+  , "repeat multiple" ~:
+      (3, [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])
+          , DEMInstrError (DEMError 0.02 [TargetDetector (DetectorId 1)])
+          ])
+      ~=? run parseDEMRepeat "repeat 3 { error(0.01) D0 error(0.02) D1 }"
+  ]
+
+-- | Parse individual targets
+testParseDEMTarget :: Test
+testParseDEMTarget = TestList
+  [ "target D0" ~:
+      TargetDetector (DetectorId 0)
+      ~=? run parseDEMTarget "D0"
+
+  , "target L3" ~:
+      TargetObservable (ObservableId 3)
+      ~=? run parseDEMTarget "L3"
+  ]
+
+-- | Parse full DEM documents
+testParseDEMFull :: Test
+testParseDEMFull = TestList
+  [ "simple dem" ~:
+      let input = unlines
+            [ "error(0.01) D0 D1 L0"
+            , "error(0.005) D1 D2"
+            , "detector(0, 0) D0"
+            , "logical_observable L0"
+            ]
+          expected = DEM
+            [ DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0), TargetDetector (DetectorId 1), TargetObservable (ObservableId 0)])
+            , DEMInstrError (DEMError 0.005 [TargetDetector (DetectorId 1), TargetDetector (DetectorId 2)])
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])
+            , DEMInstrObservable (DEMObservable (ObservableId 0))
+            ]
+      in expected ~=? run parseDEM input
+
+  , "dem with repeat" ~:
+      let input = "repeat 2 { error(0.01) D0 } detector(0, 0) D0"
+          expected = DEM
+            [ DEMInstrRepeat 2 [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])]
+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])
+            ]
+      in expected ~=? run parseDEM input
+
+  , "dem only observables" ~:
+      let input = "error(0.1) L0 L1"
+          expected = DEM [DEMInstrError (DEMError 0.1 [TargetObservable (ObservableId 0), TargetObservable (ObservableId 1)])]
+      in expected ~=? run parseDEM input
+  ]
+
+-- | High-precision probability parsing (the motivating case for Double)
+testParseDEMHighPrecision :: Test
+testParseDEMHighPrecision = TestList
+  [ "high precision probability" ~:
+      let input = "error(0.002673815958446297981) D0"
+          result = run parseDEMError input
+      in case result of
+           DEMError p [TargetDetector (DetectorId 0)] ->
+             -- Double preserves ~15 significant digits
+             assertBool "high precision preserved" (abs (p - 0.002673815958446298) < 1e-19)
+           _ -> assertFailure "parse failed"
+  ]
+
+-- | Whitespace tolerance
+testParseDEMWhitespace :: Test
+testParseDEMWhitespace = TestList
+  [ "error with extra spaces" ~:
+      DEMError 0.01 [TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error( 0.01 ) D0"
+
+  , "detector with tabs" ~:
+      DEMDetector (DetectorId 0) [1.0, 2.0]
+      ~=? run parseDEMDetector "detector(1,\t2)\tD0"
+  ]
+
+-- | Edge cases
+testParseDEMEdgeCases :: Test
+testParseDEMEdgeCases = TestList
+  [ "large detector id" ~:
+      TargetDetector (DetectorId 999999)
+      ~=? run parseDEMTarget "D999999"
+
+  , "large observable id" ~:
+      TargetObservable (ObservableId 999999)
+      ~=? run parseDEMTarget "L999999"
+
+  , "repeated same target" ~:
+      DEMError 0.1 [TargetDetector (DetectorId 0), TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(0.1) D0 D0"
+
+  , "empty dem string" ~:
+      DEM [] ~=? run parseDEM ""
+
+  , "dem with trailing newline" ~:
+      DEM [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0)])]
+      ~=? run parseDEM "error(0.01) D0\n"
+  ]
+
+-- | Block and line comments
+testParseDEMComments :: Test
+testParseDEMComments = TestList
+  [ "block comment between tokens" ~:
+      DEMError 0.01 [TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(0.01) (* comment *) D0"
+
+  , "line comment at end" ~:
+      DEMError 0.01 [TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(0.01) D0 # end of line comment"
+
+  , "block comment in tuple" ~:
+      DEMDetector (DetectorId 0) [1.0, 2.0]
+      ~=? run parseDEMDetector "detector(1, (* x *) 2) D0"
+  ]
+
+-- | Scientific notation in coordinates
+testParseDEMScientificCoords :: Test
+testParseDEMScientificCoords = TestList
+  [ "scientific notation in detector coords" ~:
+      DEMDetector (DetectorId 0) [150.0, 0.0]
+      ~=? run parseDEMDetector "detector(1.5e2, 0) D0"
+
+  , "negative scientific in coords" ~:
+      DEMDetector (DetectorId 1) [-1.5e-2, 2.0]
+      ~=? run parseDEMDetector "detector(-1.5e-2, 2) D1"
+  ]
+
+-- | Mixed target orders
+testParseDEMMixedTargets :: Test
+testParseDEMMixedTargets = TestList
+  [ "observable before detector" ~:
+      DEMError 0.1 [TargetObservable (ObservableId 0), TargetDetector (DetectorId 0)]
+      ~=? run parseDEMError "error(0.1) L0 D0"
+
+  , "interleaved targets" ~:
+      DEMError 0.1 [TargetObservable (ObservableId 0), TargetDetector (DetectorId 1), TargetObservable (ObservableId 2)]
+      ~=? run parseDEMError "error(0.1) L0 D1 L2"
+  ]
+
+-- | Empty / minimal DEMs
+testParseDEMEmpty :: Test
+testParseDEMEmpty = TestList
+  [ "empty string is empty dem" ~:
+      DEM [] ~=? run parseDEM ""
+  ]
