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 +10/−0
- src/StimParser/DEM/Expr.hs +3/−1
- src/StimParser/DEM/Parse.hs +5/−3
- src/StimParser/Parse.hs +6/−2
- stim-parser.cabal +1/−1
- test/Test/DEM/Expr.hs +40/−0
- test/Test/DEM/Parse.hs +89/−0
- test/Test/Parse.hs +30/−0
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") ]