packages feed

stim-parser 0.2.0.0 → 0.2.0.1

raw patch · 8 files changed

+184/−7 lines, 8 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,15 @@ # Revision history for stim-parser +## 0.2.0.1 -- 2026-07-18++* Fix noise-channel parsing order in `parseStim`.+  Noise-channel names such as `X_ERROR`, `Y_ERROR`, `Z_ERROR`, and+  `I_ERROR` share a prefix with gate names (`X`, `Y`, `Z`, `I`). The+  top-level dispatcher now tries noise before gates, preventing the gate+  parser from partially consuming these names and leaving the rest of the+  input orphaned.+* Add regression tests for noise-channel parsing through `parseStim`.+ ## 0.2.0.0 -- 2026-04-01  * Add DEM (Detector Error Model) parsing support.
src/StimParser/DEM/Expr.hs view
@@ -84,7 +84,9 @@ flattenInstrs coordShift detShift (i : is) =   case i of     DEMInstrShift (DEMShift cs ds) ->-      let newCoordShift = zipWith (+) (padCoords (length cs) coordShift) cs+      let newCoordShift = if null cs+                            then coordShift+                            else zipWith (+) (padCoords (length cs) coordShift) cs           newDetShift = detShift + ds       in flattenInstrs newCoordShift newDetShift is 
src/StimParser/DEM/Parse.hs view
@@ -9,7 +9,7 @@  -- | Parse a complete DEM string. parseDEM :: Parser DEM-parseDEM = DEM <$> many parseDEMInstruction+parseDEM = DEM <$> many parseDEMInstruction <* eof  -- | Parse a single DEM instruction. parseDEMInstruction :: Parser DEMInstruction@@ -51,11 +51,13 @@   oid <- ObservableId <$> (lstring "L" *> parseInt)   return $ DEMObservable oid --- | Parse a shift_detectors instruction: shift_detectors(1, 0) 0+-- | Parse a shift_detectors instruction: shift_detectors(1, 0) 0 or shift_detectors 96+--+-- Coordinates are optional. When absent, only the detector ID is shifted. parseDEMShift :: Parser DEMShift parseDEMShift = do   lstring "shift_detectors"-  coords <- parseTupleNumber+  coords <- option [] parseTupleNumber   shift <- parseInt   return $ DEMShift coords shift 
src/StimParser/Parse.hs view
@@ -288,10 +288,14 @@   let      -- the order may be tricky     parseUnit = -      try (StimG <$> parseGate)+      -- Noise must be tried before gates: noise-channel names such as+      -- X_ERROR share a prefix with gate names (X, Y, Z, I). If the gate+      -- parser runs first it can consume just the leading letter and succeed+      -- with an empty target list, leaving the rest of the input orphaned.+      try (StimNoise <$> parseNoise)+      <|> try (StimG <$> parseGate)       <|> try (StimM <$> parseMeasure)       <|> try (StimGpp <$> parseGpp)-      <|> try (StimNoise <$> parseNoise)       <|> (StimAnn <$> parseAnn)      parseUnitList = StimList <$> parseExhaust parseUnit
stim-parser.cabal view
@@ -20,7 +20,7 @@ -- PVP summary:     +-+------- breaking API changes --                  | | +----- non-breaking API additions --                  | | | +--- code changes with no API change-version:            0.2.0.0+version:            0.2.0.1  -- A short (one-line) description of the package. synopsis:           A parser combinator library for STIM quantum circuit files
test/Test/DEM/Expr.hs view
@@ -16,6 +16,7 @@   , testFlattenDEMShiftAndRepeat   , testFlattenDEMDetIdShift   , testFlattenDEMDimensionMismatch+  , testFlattenDEMBareShift   , testFlattenDEMIdempotent   ] @@ -161,6 +162,45 @@             ]           expected = DEM             [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0, 0.0])+            ]+      in expected ~=? flattenDEM input+  ]++-- | shift_detectors without coordinates should preserve accumulated coords+testFlattenDEMBareShift :: Test+testFlattenDEMBareShift = TestList+  [ "bare shift preserves coords" ~:+      let input = DEM+            [ DEMInstrShift (DEMShift [1.0, 0.5] 0)+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0, 0.0])+            , DEMInstrShift (DEMShift [] 10)+            , DEMInstrDetector (DEMDetector (DetectorId 1) [0.0, 0.0])+            ]+          expected = DEM+            [ DEMInstrDetector (DEMDetector (DetectorId 0) [1.0, 0.5])+            , DEMInstrDetector (DEMDetector (DetectorId 11) [1.0, 0.5])+            ]+      in expected ~=? flattenDEM input++  , "bare shift only det id" ~:+      let input = DEM+            [ DEMInstrShift (DEMShift [] 5)+            , DEMInstrDetector (DEMDetector (DetectorId 0) [2.0, 3.0])+            ]+          expected = DEM+            [ DEMInstrDetector (DEMDetector (DetectorId 5) [2.0, 3.0])+            ]+      in expected ~=? flattenDEM input++  , "mixed coord and bare shifts" ~:+      let input = DEM+            [ DEMInstrShift (DEMShift [1.0] 0)+            , DEMInstrShift (DEMShift [] 5)+            , DEMInstrShift (DEMShift [2.0] 0)+            , DEMInstrDetector (DEMDetector (DetectorId 0) [0.0])+            ]+          expected = DEM+            [ DEMInstrDetector (DEMDetector (DetectorId 5) [3.0])             ]       in expected ~=? flattenDEM input   ]
test/Test/DEM/Parse.hs view
@@ -1,6 +1,7 @@ module Test.DEM.Parse where  import Test.HUnit+import Text.Megaparsec (runParser)  import StimParser.DEM.Expr import StimParser.DEM.Parse@@ -27,6 +28,8 @@   , testParseDEMScientificCoords   , testParseDEMMixedTargets   , testParseDEMEmpty+  , testParseDEMCaret+  , testParseDEMEofEnforced   ]  -- | Parse individual error instructions@@ -111,6 +114,18 @@   , "shift with det offset" ~:       DEMShift [0.0, 1.0] 10       ~=? run parseDEMShift "shift_detectors(0, 1) 10"++  , "shift bare no coords" ~:+      DEMShift [] 96+      ~=? run parseDEMShift "shift_detectors 96"++  , "shift bare zero" ~:+      DEMShift [] 0+      ~=? run parseDEMShift "shift_detectors 0"++  , "shift single coord no coords" ~:+      DEMShift [0.5] 1+      ~=? run parseDEMShift "shift_detectors(0.5) 1"   ]  -- | Parse repeat blocks@@ -264,4 +279,78 @@ testParseDEMEmpty = TestList   [ "empty string is empty dem" ~:       DEM [] ~=? run parseDEM ""+  ]++-- | Caret (^) rejection — '^' indicates unflattened DEM decompositions,+-- which are not part of the stable DEM format. We reject them loudly+-- instead of silently discarding them.+testParseDEMCaret :: Test+testParseDEMCaret = TestList+  [ "single caret fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) D0 ^ D1 L0"+      case result of+        Left _ -> return ()+        Right _ -> assertFailure "parser should fail on unflattened DEM with ^"++  , "multiple carets fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) D0 ^ D1 ^ D2 L0"+      case result of+        Left _ -> return ()+        Right _ -> assertFailure "parser should fail on unflattened DEM with ^"++  , "caret between detectors fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) D0 ^ D1 ^ D2"+      case result of+        Left _ -> return ()+        Right _ -> assertFailure "parser should fail on unflattened DEM with ^"++  , "caret with observables fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) D0 ^ L0 ^ D1"+      case result of+        Left _ -> return ()+        Right _ -> assertFailure "parser should fail on unflattened DEM with ^"++  , "caret at start fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) ^ D0 D1"+      case result of+        Left _ -> return ()+        Right _ -> assertFailure "parser should fail on unflattened DEM with ^"++  , "caret at end fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) D0 D1 ^"+      case result of+        Left _ -> return ()+        Right _ -> assertFailure "parser should fail on unflattened DEM with ^"++  , "no-caret error still parses" ~:+      DEM [DEMInstrError (DEMError 0.01 [TargetDetector (DetectorId 0), TargetDetector (DetectorId 1), TargetObservable (ObservableId 0)])]+      ~=? run parseDEM "error(0.01) D0 D1 L0"+  ]++-- | EOF enforcement — parseDEM must consume all input+testParseDEMEofEnforced :: Test+testParseDEMEofEnforced = TestList+  [ "invalid trailing text fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) D0 invalid_trailing"+      case result of+        Left _ -> return ()  -- expected to fail+        Right _ -> assertFailure "parser should fail on unconsumed input"++  , "invalid token after caret fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) D0 ^ @#$"+      case result of+        Left _ -> return ()  -- expected to fail+        Right _ -> assertFailure "parser should fail on invalid token after caret"++  , "partial detector id fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01) D"+      case result of+        Left _ -> return ()  -- expected to fail+        Right _ -> assertFailure "parser should fail on partial detector id"++  , "unclosed parenthesis fails" ~: TestCase $ do+      let result = runParser parseDEM "" "error(0.01"+      case result of+        Left _ -> return ()  -- expected to fail+        Right _ -> assertFailure "parser should fail on unclosed parenthesis"   ]
test/Test/Parse.hs view
@@ -24,6 +24,7 @@   , testParseAnnTy   , testParseFInd   , testParseAnn+  , testParseStimNoiseOrder   ]  -- Helper to compare using show (since types don't derive Eq)@@ -270,4 +271,33 @@   , assertShowEqual "parseAnn OBSERVABLE_INCLUDE"        (Ann OBSERVABLE_INCLUDE Nothing [In 0] [QRec (Rec (-1))])        (run parseAnn "OBSERVABLE_INCLUDE(0) rec[-1]")+  ]++-- | Regression test for the noise-before-gate ordering bug.+-- Noise-channel names such as X_ERROR share a prefix with gate names (X, Y,+-- Z, I). The top-level 'parseStim' dispatcher must try noise first, otherwise+-- the gate parser consumes the leading letter and succeeds with an empty+-- target list, leaving the rest of the line unparsed.+testParseStimNoiseOrder :: Test+testParseStimNoiseOrder = TestList+  [ assertShowEqual "parseStim X_ERROR"+      (StimList [StimNoise (NoiseNormal X_ERROR Nothing Nothing [0.5] [Q 0])])+      (run parseStim "!!!Start\nX_ERROR(0.5) 0\n")++  , assertShowEqual "parseStim Y_ERROR"+      (StimList [StimNoise (NoiseNormal Y_ERROR Nothing Nothing [0.01] [Q 1])])+      (run parseStim "!!!Start\nY_ERROR(0.01) 1\n")++  , assertShowEqual "parseStim Z_ERROR"+      (StimList [StimNoise (NoiseNormal Z_ERROR Nothing Nothing [0.02] [Q 2])])+      (run parseStim "!!!Start\nZ_ERROR(0.02) 2\n")++  , assertShowEqual "parseStim I_ERROR"+      (StimList [StimNoise (NoiseNormal I_ERROR Nothing Nothing [0.1] [Q 0])])+      (run parseStim "!!!Start\nI_ERROR(0.1) 0\n")++  -- Sanity check: ordinary gate lines still work after the reordering.+  , assertShowEqual "parseStim H gate"+      (StimList [StimG (Gate H Nothing [Q 0, Q 1, Q 2])])+      (run parseStim "!!!Start\nH 0 1 2\n")   ]