Skip to content

Conversation

@yarkivaev
Copy link
Contributor

@yarkivaev yarkivaev commented Sep 7, 2025

Closes #73

Summary by CodeRabbit

  • New Features

    • Added a new CLI subcommand "match" to search for patterns in a program.
    • Supports --pattern and optional --when for conditional matching.
    • Works with existing input formats and files or stdin; prints matches to stdout.
    • Respects --log-level for consistent logging.
  • Tests

    • Added comprehensive tests for the match command covering patterns, bindings, inputs, flags, special characters, and error cases.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 7, 2025

Walkthrough

Adds a new CLI subcommand "match" that reads a program, parses a pattern and optional YAML condition, runs pattern matching, and prints results. Implements option parsing, logging propagation, condition parsing via Yaml.parseConditionString, match execution (performMatch'), and extensive CLI tests for match behavior.

Changes

Cohort / File(s) Change Summary
CLI: new match subcommand
src/CLI.hs
Adds OptsMatch and CmdMatch; adds matchParser (flags: --pattern, optional --when, input format/file); wires match into command parser, propagates logLevel, reads input, parses program, parses optional condition via Yaml.parseConditionString, calls performMatch' (SALTY), and prints formatted matches.
YAML: condition parsing
src/Yaml.hs
Changes module export header to module Yaml (module Yaml) where; adds parseConditionString :: String -> IO Condition using Yaml.decodeEither' and Data.ByteString.Char8 packing; preserves existing YAML rule/normalization logic.
Tests: match CLI coverage
test/CLISpec.hs
Adds comprehensive match test suite covering binding/expression patterns, meta variants, multi-bindings, invalid patterns/conditions, input formats (files, XMIR, stdin), empty programs, CLI flags (--when, --log-level), and error cases; existing rewrite tests unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as CLI (match)
  participant IO as IO
  participant Parser as Program Parser
  participant YAML as Yaml.parseConditionString
  participant Matcher as performMatch'
  participant Out as Stdout

  User->>CLI: phino match --pattern P [--when C] [FILE]
  CLI->>IO: Read input (FILE or stdin)
  IO-->>CLI: Program text
  CLI->>Parser: Parse program
  Parser-->>CLI: Program AST
  alt --when provided
    CLI->>YAML: parseConditionString(C)
    YAML-->>CLI: Condition
  else
    CLI-->>CLI: Condition = None
  end
  CLI->>Matcher: performMatch'(pattern=P, condition?)
  Matcher-->>CLI: Matches (formatted)
  CLI->>Out: Print matches
  Out-->>User: Match results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • maxonfjvipon

Poem

A whisk of whiskers, ears alert and keen,
I grep the fields where matching lines are seen.
Patterns and YAML scents I chase and sniff,
Hopping through ASTs, I find the rift.
Thump-thump — matches found, I leave a gift. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "add match command to phino" is concise and accurately summarizes the primary change—adding a new CLI subcommand—making it clear to reviewers and history scanners.
Linked Issues Check ✅ Passed The PR implements a phino "match" subcommand with --pattern and optional --when, supports reading input from file or stdin, parses conditions via a new parseConditionString in Yaml, invokes existing Matcher functionality (imports and performMatch' usage), and adds tests exercising the feature, so it satisfies the objectives of issue #73.
Out of Scope Changes Check ✅ Passed All modified files (src/CLI.hs, src/Yaml.hs, and test/CLISpec.hs) and the added functionality are directly related to implementing the match command and its optional --when condition; no unrelated or out-of-scope changes are evident in the provided summaries.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@yarkivaev
Copy link
Contributor Author

@maxonfjvipon Hello! Could you please check this pull request?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
src/Yaml.hs (1)

15-15: Avoid Char8 packing for YAML text

Data.ByteString.Char8.pack can corrupt non-ASCII. Prefer encodeUtf8 . T.pack as shown above.

test/CLISpec.hs (2)

192-201: Test name vs. expectation mismatch for --when

The example uses a YAML string that likely won’t parse to the intended meta-variable condition, and the test expects no output. Either rename to indicate invalid condition or provide a valid YAML that filters to a specific binding.

Example adjustment (filters to only b ↦ 2):

-it "handles when condition for filtering" $
+it "filters matches with a valid --when condition" $
   withStdin "Q -> [[a -> 1, b -> 2, c -> 3]]" $
-    testCLI ["match", "--pattern", "!x ↦ !y", "--when", "eq: [\"!x\", \"b\"]"] 
-      []  -- When conditions need proper YAML format
+    testCLI ["match", "--pattern", "!x ↦ !y", "--when", "eq: [\"!x\", b]"]
+      ["b ↦"]

240-243: “stdin read errors” test likely not exercising an error

Feeding empty stdin does not trigger an IO read failure; it yields empty input and then parsing decides the outcome. Consider inducing an actual read error or rename the test to reflect “empty stdin produces no matches.”

src/CLI.hs (2)

152-162: CLI parser UX

Helpful help texts; consider adding metavar "YAML" for --when to hint the expected format (optional).


277-287: Print trailing newline for CLI ergonomics

putStr can leave the shell prompt glued to the last line. Prefer putStrLn.

-      result <- performMatch pattern cond prog SALTY
-      putStr result
+      result <- performMatch pattern cond prog SALTY
+      putStrLn result
src/Match.hs (1)

115-121: Dead code: formatMatchResult is unused

Either remove it or use it inside your final renderer. If you keep it, consider extending it to include substitution formatting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1841ed and ada545d.

📒 Files selected for processing (5)
  • phino.cabal (1 hunks)
  • src/CLI.hs (8 hunks)
  • src/Match.hs (1 hunks)
  • src/Yaml.hs (2 hunks)
  • test/CLISpec.hs (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (macos-15)
🔇 Additional comments (8)
phino.cabal (1)

44-46: Expose Match module — looks good

Library exposure of Match is correct and aligns with the new CLI subcommand.

test/CLISpec.hs (1)

97-171: Solid coverage of happy paths for match

Broad cases (attributes, ξ/Φ/⊥, nesting, ASCII) look good and use substring assertions to avoid brittle formatting checks.

src/CLI.hs (5)

23-24: Imports for performMatch and parseConditionString

Wiring the new command through these imports is correct.

Also applies to: 35-36


52-53: Command variant added

CmdMatch integration into Command is clean and consistent.


70-77: Opts for match look right

Flags cover log level, pattern, optional condition, format, and input file. Good.


193-193: Command registration

match is correctly registered with a clear description.


213-214: Log level propagation

Including CmdMatch in setLogLevel' is correct.

src/Match.hs (1)

99-114: Confirm RuleContext scope for conditions

filterByCondition builds RuleContext using Program matchedScope. If conditions are intended to see the whole program (not just the nearest formation), this should use the original Program. Please confirm and adjust API to pass prog in.

Run this to inspect meetCondition/RuleContext expectations and usages:

#!/bin/bash
set -euo pipefail
rg -n -C3 'data\s+RuleContext|newtype\s+RuleContext|type\s+RuleContext' src
rg -n -C2 '\bmeetCondition\b' src
rg -n -C2 'RuleContext\s*\(' src

src/Match.hs Outdated
Comment on lines 39 to 69
findMatches (BindingPattern ptnBinding) prog@(Program rootExpr) =
findBindingMatches ptnBinding rootExpr
where
-- Find all bindings that match the pattern
findBindingMatches :: Binding -> Expression -> [MatchResult]
findBindingMatches pattern expr = case expr of
ExFormation bds ->
let directMatches = concatMap (matchSingleBinding pattern) bds
deepMatches = concatMap (findInBinding pattern) bds
in directMatches ++ deepMatches
ExDispatch e _ -> findBindingMatches pattern e
ExApplication e bd ->
findBindingMatches pattern e ++ findInBinding pattern bd
_ -> []

-- Try to match a single binding
matchSingleBinding :: Binding -> Binding -> [MatchResult]
matchSingleBinding pattern target =
case matchBinding pattern target defaultScope of
[] -> []
substs -> map (\s -> MatchResult (bindingToExpression target) defaultScope s) substs

-- Find matches within a binding
findInBinding :: Binding -> Binding -> [MatchResult]
findInBinding pattern (BiTau _ e) = findBindingMatches pattern e
findInBinding _ _ = []

-- Convert binding to expression for display
bindingToExpression :: Binding -> Expression
bindingToExpression bd = ExFormation [bd]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect scope propagation: matchedScope is always defaultScope, breaking --when filtering

You build MatchResult with defaultScope and also call matchBinding using defaultScope. As a result, filterByCondition evaluates conditions against the wrong scope. Thread the actual scope through traversal and use it both for matching and in MatchResult.

Apply this diff:

-findMatches (BindingPattern ptnBinding) prog@(Program rootExpr) = 
-  findBindingMatches ptnBinding rootExpr
+findMatches (BindingPattern ptnBinding) prog@(Program rootExpr) =
+  findBindingMatches ptnBinding rootExpr rootExpr
   where
     -- Find all bindings that match the pattern
-    findBindingMatches :: Binding -> Expression -> [MatchResult]
-    findBindingMatches pattern expr = case expr of
-      ExFormation bds -> 
-        let directMatches = concatMap (matchSingleBinding pattern) bds
-            deepMatches = concatMap (findInBinding pattern) bds
-        in directMatches ++ deepMatches
-      ExDispatch e _ -> findBindingMatches pattern e
-      ExApplication e bd -> 
-        findBindingMatches pattern e ++ findInBinding pattern bd
+    findBindingMatches :: Binding -> Expression -> Expression -> [MatchResult]
+    findBindingMatches pattern expr scope = case expr of
+      ExFormation bds ->
+        let scope' = ExFormation bds
+            directMatches = concatMap (matchSingleBinding pattern scope') bds
+            deepMatches  = concatMap (findInBinding pattern scope') bds
+        in directMatches ++ deepMatches
+      ExDispatch e _ ->
+        findBindingMatches pattern e scope
+      ExApplication e bd ->
+        findBindingMatches pattern e scope ++ findInBinding pattern scope bd
       _ -> []
     
     -- Try to match a single binding
-    matchSingleBinding :: Binding -> Binding -> [MatchResult]
-    matchSingleBinding pattern target =
-      case matchBinding pattern target defaultScope of
+    matchSingleBinding :: Binding -> Expression -> Binding -> [MatchResult]
+    matchSingleBinding pattern scope target =
+      case matchBinding pattern target scope of
         [] -> []
-        substs -> map (\s -> MatchResult (bindingToExpression target) defaultScope s) substs
+        substs -> map (\s -> MatchResult (bindingToExpression target) scope s) substs
     
     -- Find matches within a binding
-    findInBinding :: Binding -> Binding -> [MatchResult]
-    findInBinding pattern (BiTau _ e) = findBindingMatches pattern e
-    findInBinding _ _ = []
+    findInBinding :: Binding -> Expression -> Binding -> [MatchResult]
+    findInBinding pattern scope (BiTau _ e) = findBindingMatches pattern e scope
+    findInBinding _ _ _ = []
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
findMatches (BindingPattern ptnBinding) prog@(Program rootExpr) =
findBindingMatches ptnBinding rootExpr
where
-- Find all bindings that match the pattern
findBindingMatches :: Binding -> Expression -> [MatchResult]
findBindingMatches pattern expr = case expr of
ExFormation bds ->
let directMatches = concatMap (matchSingleBinding pattern) bds
deepMatches = concatMap (findInBinding pattern) bds
in directMatches ++ deepMatches
ExDispatch e _ -> findBindingMatches pattern e
ExApplication e bd ->
findBindingMatches pattern e ++ findInBinding pattern bd
_ -> []
-- Try to match a single binding
matchSingleBinding :: Binding -> Binding -> [MatchResult]
matchSingleBinding pattern target =
case matchBinding pattern target defaultScope of
[] -> []
substs -> map (\s -> MatchResult (bindingToExpression target) defaultScope s) substs
-- Find matches within a binding
findInBinding :: Binding -> Binding -> [MatchResult]
findInBinding pattern (BiTau _ e) = findBindingMatches pattern e
findInBinding _ _ = []
-- Convert binding to expression for display
bindingToExpression :: Binding -> Expression
bindingToExpression bd = ExFormation [bd]
findMatches (BindingPattern ptnBinding) prog@(Program rootExpr) =
findBindingMatches ptnBinding rootExpr rootExpr
where
-- Find all bindings that match the pattern
findBindingMatches :: Binding -> Expression -> Expression -> [MatchResult]
findBindingMatches pattern expr scope = case expr of
ExFormation bds ->
let scope' = ExFormation bds
directMatches = concatMap (matchSingleBinding pattern scope') bds
deepMatches = concatMap (findInBinding pattern scope') bds
in directMatches ++ deepMatches
ExDispatch e _ ->
findBindingMatches pattern e scope
ExApplication e bd ->
findBindingMatches pattern e scope ++ findInBinding pattern scope bd
_ -> []
-- Try to match a single binding
matchSingleBinding :: Binding -> Expression -> Binding -> [MatchResult]
matchSingleBinding pattern scope target =
case matchBinding pattern target scope of
[] -> []
substs -> map (\s -> MatchResult (bindingToExpression target) scope s) substs
-- Find matches within a binding
findInBinding :: Binding -> Expression -> Binding -> [MatchResult]
findInBinding pattern scope (BiTau _ e) =
findBindingMatches pattern e scope
findInBinding _ _ _ = []
-- Convert binding to expression for display
bindingToExpression :: Binding -> Expression
bindingToExpression bd = ExFormation [bd]
🤖 Prompt for AI Agents
In src/Match.hs around lines 39 to 69, the traversal always uses defaultScope
for matching and for constructing MatchResult; instead thread the current Scope
through the helper functions: change findBindingMatches, matchSingleBinding, and
findInBinding to accept a Scope parameter, start the top-level call with the
Program's actual scope (not defaultScope), pass that scope into recursive calls
for ExFormation, ExDispatch and ExApplication cases, call matchBinding with the
current scope, and construct MatchResult using that same scope
(bindingToExpression stays the same). Ensure every recursive call that descends
into expressions or bindings propagates the current scope rather than
substituting defaultScope.

src/Match.hs Outdated
Comment on lines 70 to 98
findMatches (ExpressionPattern ptnExpr) prog@(Program rootExpr) =
let substs = matchProgram ptnExpr prog
in concatMap (findExprLocations ptnExpr rootExpr) substs
where
-- Find specific expression locations that match
findExprLocations :: Expression -> Expression -> Subst -> [MatchResult]
findExprLocations pattern expr subst =
let directMatch = case matchExpression pattern expr defaultScope of
[] -> []
matches -> if subst `elem` matches
then [MatchResult expr defaultScope subst]
else []
deepMatches = case expr of
ExFormation bds ->
concatMap (\bd -> findInBinding pattern bd (ExFormation bds) subst) bds
ExDispatch e _ ->
findExprLocations pattern e subst
ExApplication e bd ->
findExprLocations pattern e subst ++
findInBinding pattern bd defaultScope subst
_ -> []
in directMatch ++ deepMatches

-- Find matches within a binding
findInBinding :: Expression -> Binding -> Expression -> Subst -> [MatchResult]
findInBinding pattern (BiTau _ e) scope subst =
findExprLocations pattern e subst
findInBinding _ _ _ _ = []

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Propagate scope for expression matches too

Same issue for expression patterns: results carry defaultScope and matchExpression uses defaultScope. Thread the current scope and store it in MatchResult.

-findMatches (ExpressionPattern ptnExpr) prog@(Program rootExpr) = 
-  let substs = matchProgram ptnExpr prog
-  in concatMap (findExprLocations ptnExpr rootExpr) substs
+findMatches (ExpressionPattern ptnExpr) prog@(Program rootExpr) =
+  let substs = matchProgram ptnExpr prog
+  in concatMap (findExprLocations ptnExpr rootExpr rootExpr) substs
   where
     -- Find specific expression locations that match
-    findExprLocations :: Expression -> Expression -> Subst -> [MatchResult]
-    findExprLocations pattern expr subst = 
-      let directMatch = case matchExpression pattern expr defaultScope of
+    findExprLocations :: Expression -> Expression -> Expression -> Subst -> [MatchResult]
+    findExprLocations pattern expr scope subst =
+      let directMatch = case matchExpression pattern expr scope of
             [] -> []
-            matches -> if subst `elem` matches
-                      then [MatchResult expr defaultScope subst]
-                      else []
+            matches ->
+              if subst `elem` matches
+                then [MatchResult expr scope subst]
+                else []
           deepMatches = case expr of
-            ExFormation bds -> 
-              concatMap (\bd -> findInBinding pattern bd (ExFormation bds) subst) bds
-            ExDispatch e _ -> 
-              findExprLocations pattern e subst
-            ExApplication e bd -> 
-              findExprLocations pattern e subst ++ 
-              findInBinding pattern bd defaultScope subst
+            ExFormation bds ->
+              let scope' = ExFormation bds
+              in concatMap (\bd -> findInBinding pattern bd scope' subst) bds
+            ExDispatch e _ ->
+              findExprLocations pattern e scope subst
+            ExApplication e bd ->
+              findExprLocations pattern e scope subst
+                ++ findInBinding pattern bd scope subst
             _ -> []
       in directMatch ++ deepMatches
     
     -- Find matches within a binding
-    findInBinding :: Expression -> Binding -> Expression -> Subst -> [MatchResult]
-    findInBinding pattern (BiTau _ e) scope subst = 
-      findExprLocations pattern e subst
-    findInBinding _ _ _ _ = []
+    findInBinding :: Expression -> Binding -> Expression -> Subst -> [MatchResult]
+    findInBinding pattern (BiTau _ e) scope subst =
+      findExprLocations pattern e scope subst
+    findInBinding _ _ _ _ = []
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
findMatches (ExpressionPattern ptnExpr) prog@(Program rootExpr) =
let substs = matchProgram ptnExpr prog
in concatMap (findExprLocations ptnExpr rootExpr) substs
where
-- Find specific expression locations that match
findExprLocations :: Expression -> Expression -> Subst -> [MatchResult]
findExprLocations pattern expr subst =
let directMatch = case matchExpression pattern expr defaultScope of
[] -> []
matches -> if subst `elem` matches
then [MatchResult expr defaultScope subst]
else []
deepMatches = case expr of
ExFormation bds ->
concatMap (\bd -> findInBinding pattern bd (ExFormation bds) subst) bds
ExDispatch e _ ->
findExprLocations pattern e subst
ExApplication e bd ->
findExprLocations pattern e subst ++
findInBinding pattern bd defaultScope subst
_ -> []
in directMatch ++ deepMatches
-- Find matches within a binding
findInBinding :: Expression -> Binding -> Expression -> Subst -> [MatchResult]
findInBinding pattern (BiTau _ e) scope subst =
findExprLocations pattern e subst
findInBinding _ _ _ _ = []
findMatches (ExpressionPattern ptnExpr) prog@(Program rootExpr) =
let substs = matchProgram ptnExpr prog
in concatMap (findExprLocations ptnExpr rootExpr rootExpr) substs
where
-- Find specific expression locations that match
findExprLocations :: Expression -> Expression -> Expression -> Subst -> [MatchResult]
findExprLocations pattern expr scope subst =
let directMatch = case matchExpression pattern expr scope of
[] -> []
matches ->
if subst `elem` matches
then [MatchResult expr scope subst]
else []
deepMatches = case expr of
ExFormation bds ->
let scope' = ExFormation bds
in concatMap (\bd -> findInBinding pattern bd scope' subst) bds
ExDispatch e _ ->
findExprLocations pattern e scope subst
ExApplication e bd ->
findExprLocations pattern e scope subst
+ findInBinding pattern bd scope subst
_ -> []
in directMatch ++ deepMatches
-- Find matches within a binding
findInBinding :: Expression -> Binding -> Expression -> Subst -> [MatchResult]
findInBinding pattern (BiTau _ e) scope subst =
findExprLocations pattern e scope subst
findInBinding _ _ _ _ = []
🤖 Prompt for AI Agents
In src/Match.hs around lines 70 to 98, expression-matching currently always uses
and records defaultScope; change findExprLocations and findInBinding to accept a
current Scope parameter, pass the appropriate scope when recursing (e.g.
propagate scope from the surrounding expression or binding), call
matchExpression with that current scope instead of defaultScope, and construct
MatchResult using the threaded scope so returned MatchResult entries carry the
actual scope rather than defaultScope.

src/Match.hs Outdated
Comment on lines 122 to 130
-- | Format match results showing unique matches
formatUniqueMatches :: [MatchResult] -> PrintMode -> String
formatUniqueMatches results mode =
let uniqueExprs = nub (map matchedExpression results)
formatted = map (\e -> case mode of
SWEET -> prettyExpression' e
SALTY -> prettyExpression e) uniqueExprs
in unlines formatted

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Output doesn’t meet Issue #73: mappings are not printed

formatUniqueMatches renders only expressions, and performMatch returns that string. The spec expects lines like “t ↦ ξ.k” showing the substitution. Please format and print the substitution(s) from MatchResult.substitution (ideally stable-sorted and optionally deduped by (matchedExpression, substitution)).

I can wire a formatter that pretty-prints each (var ↦ term) pair from Subst in SALTY/SWEET modes and update performMatch to use it. Want me to draft that?

Also applies to: 151-154

🤖 Prompt for AI Agents
In src/Match.hs around lines 122-130 (and similarly 151-154),
formatUniqueMatches currently prints only matched expressions but the spec
(Issue #73) requires printing substitutions like "t ↦ ξ.k"; change it to iterate
MatchResult values and for each produce a stable-sorted, optionally deduplicated
output of (matchedExpression, substitution) pairs, where the substitution is
rendered as a list of "var ↦ term" entries; use nubBy on the tuple
(matchedExpression, substitution) if dedupe is desired, sort keys
deterministically (e.g. sortBy on variable name) to produce stable order, and
call the existing prettyExpression / prettyExpression' depending on PrintMode to
format terms and variable names, then join entries into lines and return unlines
of those lines; also update performMatch to call this new substitution-aware
formatter instead of the old expression-only function.

src/Yaml.hs Outdated
-- SPDX-License-Identifier: MIT

module Yaml where
module Yaml (module Yaml, parseConditionString) where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Invalid self re-export in module header breaks compilation

module Yaml (module Yaml, parseConditionString) attempts to re-export the current module, which isn't allowed (only imported modules can be re-exported). Use an explicit export list or revert to exporting everything.

Apply this minimal fix:

-module Yaml (module Yaml, parseConditionString) where
+module Yaml where
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
module Yaml (module Yaml, parseConditionString) where
module Yaml where
🧰 Tools
🪛 GitHub Check: build (macos-15)

[warning] 9-9:
‘parseConditionString’ is exported by ‘parseConditionString’ and ‘module Yaml’

🤖 Prompt for AI Agents
In src/Yaml.hs around line 9, the module header "module Yaml (module Yaml,
parseConditionString) where" illegally re-exports itself; replace the header
with a normal export that avoids self re-export—e.g. change the line to "module
Yaml where" (or alternatively provide an explicit export list that includes
parseConditionString and any other intended exports) so the module compiles.

src/Yaml.hs Outdated
Comment on lines 197 to 201
-- | Parse a condition from a string (for use with --when flag)
parseConditionString :: String -> IO Condition
parseConditionString str = do
-- Try to parse as YAML
case Yaml.decodeEither' (BSC.pack str) of
Right cond -> pure cond
Left err -> error $ "Failed to parse condition: " ++ show err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prefer UTF-8 and IO exceptions over error in parseConditionString

Use proper UTF-8 encoding and raise exceptions in the IO monad to integrate with the CLI handler.

Apply:

-parseConditionString :: String -> IO Condition
-parseConditionString str = do
-  -- Try to parse as YAML
-  case Yaml.decodeEither' (BSC.pack str) of
-    Right cond -> pure cond
-    Left err -> error $ "Failed to parse condition: " ++ show err
+parseConditionString :: String -> IO Condition
+parseConditionString str =
+  case Yaml.decodeEither' (encodeUtf8 (T.pack str)) of
+    Right cond -> pure cond
+    Left err   -> throwIO (userError ("Failed to parse condition: " <> show err))

Additionally, add imports (and drop BSC if unused elsewhere):

- import qualified Data.ByteString.Char8 as BSC
+ import Control.Exception (throwIO)
+ import qualified Data.Text as T
+ import Data.Text.Encoding (encodeUtf8)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- | Parse a condition from a string (for use with --when flag)
parseConditionString :: String -> IO Condition
parseConditionString str = do
-- Try to parse as YAML
case Yaml.decodeEither' (BSC.pack str) of
Right cond -> pure cond
Left err -> error $ "Failed to parse condition: " ++ show err
-- In your imports, replace or add:
import Control.Exception (throwIO)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
-- | Parse a condition from a string (for use with --when flag)
parseConditionString :: String -> IO Condition
parseConditionString str =
case Yaml.decodeEither' (encodeUtf8 (T.pack str)) of
Right cond -> pure cond
Left err -> throwIO (userError ("Failed to parse condition: " <> show err))
🤖 Prompt for AI Agents
In src/Yaml.hs around lines 197 to 203, replace the use of BSC.pack and the call
to error with UTF-8 encoding and an IO exception: convert the input String to
Text and call Data.Text.Encoding.encodeUtf8 before passing to
Yaml.decodeEither', and on Left use throwIO (e.g. throwIO . userError $ "Failed
to parse condition: " ++ show err) instead of error; add imports for Data.Text
(qualified or Data.Text.pack), Data.Text.Encoding (encodeUtf8), and
Control.Exception (throwIO), and remove the BSC import if it is no longer used
elsewhere.

@maxonfjvipon
Copy link
Member

@yarkivaev we already have Matcher module, which implements all the functionally for matching expressions, so let's use it

@yarkivaev
Copy link
Contributor Author

@maxonfjvipon Hello! I've removed Match.hs. Could you please check the pull request again?

@maxonfjvipon
Copy link
Member

@yarkivaev I'd recommend you to investigate how rewrite command and rewriting phi expression works.
There are 4 ingredients: program, pattern and result and optional condition. First we match pattern agains program and build substitution. Substitution is a map where key is some meta (e.g. !e for meta expression, !a for meta attribute and so on); value is corresponding expression, attribute, binding, etc. All of this is done in Matcher module.
Then using built substitution we build pattern expression and result expression. It's done in Builder module.
Then we traverse though original program once again, searching for built pattern expression. When we find it - we just replace it with built result expression. It's done in Replacer module. That's it.

As you see - matching is only one step in this rewriting process. When something's wrong, it's quite hard to tell where exactly error is. In order to add more traceability to the phino we want to introduce match command which would exactly the same what's already implemented in rewriting pipeline. So you don't need to introduce new mechanism of matching or new MatchLocation as a result of matching. You already have all the needed functions, like: matchProgram or matchExpressionDeep. So just combine it all together into a new CLI command so we can use matching separately from rewriting pipeline.

The only new thing here is parsing when condition. We don't have it since rewriting is implemented via yaml files.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/CLI.hs (3)

157-167: CLI UX: consider PATTERN as a positional arg.

grep-like tools usually accept the pattern positionally. Keeping --pattern is fine; adding a positional PATTERN (and resolving precedence) improves ergonomics.


75-82: Optional: support --sweet printing for match output.

You always format matches in SALTY. Consider a --sweet flag (like rewrite) to mirror output style options.


12-41: Tidy imports (minor).

Control.Exception.Base, matchExpressionDeep, and Builder (buildExpression, buildBinding) appear unused here; also you split Parser/Pretty across two imports. Consolidating/removing unused imports avoids -Wall warnings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ada545d and 81dd32e.

⛔ Files ignored due to path filters (1)
  • stack.yaml.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • src/CLI.hs (10 hunks)
  • src/Yaml.hs (2 hunks)
  • test/CLISpec.hs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Yaml.hs
  • test/CLISpec.hs
🔇 Additional comments (2)
src/CLI.hs (2)

197-198: Subcommand wiring LGTM.

Command is registered with a clear description.


214-219: Log-level propagation LGTM.

CmdMatch participates in global log-level handling correctly.

Comment on lines 282 to 290
CmdMatch OptsMatch {..} -> do
input <- readInput inputFile
prog <- parseProgram input inputFormat
cond <- case whenCondition of
Nothing -> pure Nothing
Just condStr -> Just <$> parseConditionString condStr
result <- performMatch' pattern cond prog SALTY
putStr result
where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Make match exit non-zero when no matches (grep semantics).

Currently prints nothing and exits 0 on no matches. Return exit code 1 when no matches to align with grep-like behavior required by the PR objectives.

Apply this diff:

@@
     CmdMatch OptsMatch {..} -> do
       input <- readInput inputFile
       prog <- parseProgram input inputFormat
       cond <- case whenCondition of
         Nothing -> pure Nothing
         Just condStr -> Just <$> parseConditionString condStr
-      result <- performMatch' pattern cond prog SALTY
-      putStr result
+      result <- performMatch' pattern cond prog SALTY
+      if null (filter (not . isSpace) result)
+        then exitWith (ExitFailure 1)
+        else putStr result

Additionally add missing imports outside this hunk:

import Data.Char (isSpace, toLower, toUpper)
import System.Exit (ExitCode (..), exitFailure, exitWith)
🤖 Prompt for AI Agents
In src/CLI.hs around lines 282 to 290, the CmdMatch branch currently prints
match output but always exits 0 even when there are no matches; update it so
that if result is empty (no matches) the program exits with a non-zero status
(exit code 1) to match grep semantics, otherwise print the result and exit
normally; implement this by checking whether result is empty and calling
exitFailure or exitWith (ExitFailure 1) when empty, and retain the existing
successful exit behavior when non-empty. Also add the missing imports at the top
of the file: import Data.Char (isSpace, toLower, toUpper) and import System.Exit
(ExitCode (..), exitFailure, exitWith).

Comment on lines +320 to +339
performMatch' :: String -> Maybe Condition -> Program -> PrintMode -> IO String
performMatch' patternStr maybeCond prog@(Program rootExpr) mode = do
logDebug (printf "Searching for pattern: %s" patternStr)
case parseExpression patternStr of
Right expr -> do
let substs = matchProgram expr prog
logDebug (printf "Found %d matches for expression pattern" (length substs))
filtered <- filterByCondition' maybeCond substs prog
logDebug (printf "After filtering: %d matches" (length filtered))
let exprs = extractMatchedExpressions expr filtered
formatMatches exprs mode
Left _ -> case parseBinding patternStr of
Right binding -> do
let substs = findBindingMatches binding rootExpr
logDebug (printf "Found %d matches for binding pattern" (length substs))
filtered <- filterByCondition' maybeCond (map snd substs) prog
logDebug (printf "After filtering: %d matches" (length filtered))
let matchedBindings = [bd | (bd, s) <- substs, s `elem` filtered]
formatBindingMatches matchedBindings mode
Left err -> throwIO $ userError $ "Invalid pattern: " ++ err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Print mappings “name ↦ value” instead of raw nodes.

The command currently emits only pretty-printed nodes, losing the meta-name → matched value mapping that the issue’s example requires (e.g., “t ↦ ξ.k”). Emit per-substitution mappings.

Apply this diff:

@@
       case parseExpression patternStr of
         Right expr -> do
           let substs = matchProgram expr prog
           logDebug (printf "Found %d matches for expression pattern" (length substs))
           filtered <- filterByCondition' maybeCond substs prog
           logDebug (printf "After filtering: %d matches" (length filtered))
-          let exprs = extractMatchedExpressions expr filtered
-          formatMatches exprs mode
+          formatSubsts filtered mode
         Left _ -> case parseBinding patternStr of
           Right binding -> do
             let substs = findBindingMatches binding rootExpr
             logDebug (printf "Found %d matches for binding pattern" (length substs))
             filtered <- filterByCondition' maybeCond (map snd substs) prog
             logDebug (printf "After filtering: %d matches" (length filtered))
-            let matchedBindings = [bd | (bd, s) <- substs, s `elem` filtered]
-            formatBindingMatches matchedBindings mode
+            formatSubsts filtered mode
           Left err -> throwIO $ userError $ "Invalid pattern: " ++ err
@@
-    extractMatchedExpressions :: Expression -> [Subst] -> [Expression]
-    extractMatchedExpressions pattern substs = 
-      [expr | Subst m <- substs,
-              MvExpression expr _ <- Map.elems m]
+    formatSubsts :: [Subst] -> PrintMode -> IO String
+    formatSubsts [] _ = pure ""
+    formatSubsts substs mode = do
+      let fmtExpr e = case mode of
+                        SWEET -> prettyExpression' e
+                        SALTY -> prettyExpression e
+      let lineOf (name, MvExpression e _) = Just (show name <> " ↦ " <> fmtExpr e)
+          lineOf (name, MvBinding b _)    = Just (show name <> " ↦ " <> prettyBinding b)
+          lineOf _                        = Nothing
+      let lines' =
+            [ ln
+            | Subst m <- substs
+            , (name, mv) <- Map.toList m
+            , Just ln <- [lineOf (name, mv)]
+            ]
+      pure (unlines lines')
-    filterByCondition' :: Maybe Condition -> [Subst] -> Program -> IO [Subst]
+    filterByCondition' :: Maybe Condition -> [Subst] -> Program -> IO [Subst]
@@
-    formatMatches :: [Expression] -> PrintMode -> IO String
-    formatMatches [] _ = pure ""
-    formatMatches exprs mode = do
-      let formatted = map (\e -> case mode of
-                                  SWEET -> prettyExpression' e
-                                  SALTY -> prettyExpression e) exprs
-      pure $ unlines formatted
-    formatBindingMatches :: [Binding] -> PrintMode -> IO String
-    formatBindingMatches [] _ = pure ""
-    formatBindingMatches bds mode = do
-      let formatted = map prettyBinding bds
-      pure $ unlines formatted

Note: This assumes the map key type in Subst has a sane Show instance (e.g., renders !a as a). If not, swap show name with the appropriate pretty-printer for meta-names. See verification comment below.

Also applies to: 355-375

Comment on lines +341 to +354
findBindingMatches pattern expr = case expr of
ExFormation bds ->
let direct = [(bd, s) | bd <- bds, s <- matchBinding pattern bd defaultScope]
nested = concatMap (searchInBinding pattern) bds
in direct ++ nested
ExDispatch e _ -> findBindingMatches pattern e
ExApplication e bd ->
findBindingMatches pattern e ++ searchInBinding pattern bd
_ -> []
where
searchInBinding :: Binding -> Binding -> [(Binding, Subst)]
searchInBinding ptn (BiTau _ e) = findBindingMatches ptn e
searchInBinding _ _ = []
extractMatchedExpressions :: Expression -> [Subst] -> [Expression]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Custom binding traversal may miss cases; prefer existing deep match utilities.

findBindingMatches manually walks a subset of constructors (ExFormation, ExDispatch, ExApplication) and only recurses into BiTau. This risks missing bindings under other nodes now or in the future. If Matcher exposes a deep-binding matcher (akin to matchExpressionDeep), reuse it; otherwise, build a uniform traversal that visits every binding occurrence.

Run this to inspect available constructors and matcher APIs, then decide whether to replace findBindingMatches:


🏁 Script executed:

#!/bin/bash
# Show Expression/Binding constructors (context for traversal coverage)
rg -nP -C2 'data\s+Expression\b|data\s+Binding\b' src

# Inspect Subst/MetaValue shape and key type (to choose a proper name pretty-printer)
rg -nP -C2 '\bdata\s+Subst\b|\bnewtype\s+Subst\b|\bdata\s+MetaValue\b|\btype\s+MetaVar\b|\bnewtype\s+Meta\b' src

# Look for any deep-binding matcher helpers
rg -nP -C2 '\bmatch.*Binding.*Deep\b|\bmatchBindingDeep\b|\bmatch.*Deep\b' src

Length of output: 2302


Replace custom findBindingMatches with Matcher.deep-match helpers.

Matcher already exposes matchExpressionDeep and matchBindingExpression which implement the deep traversal for ExFormation / ExDispatch / ExApplication and BiTau — reuse those instead of the manual recursion to avoid duplicating logic and missing cases. Location: src/CLI.hs (lines 341–354).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/CLI.hs (3)

329-337: Non‑matches exit code: decide whether to emulate grep semantics.

Prior feedback recommended exiting with code 1 when no matches. That’s useful for scripting but would require adjusting the top-level handler to avoid logging an error on ExitFailure 1 and updating tests that currently expect success on no matches.

Do we want grep-like exit code (1) on zero matches? If yes, I’ll wire it in and update the handler and tests accordingly; otherwise, we keep current success semantics.


380-399: Emit meta-variable mappings instead of raw nodes.

Right now expression patterns print only the matched nodes and binding patterns print pretty bindings. To align with the PR objective’s “mappings” output and make results uniform, format the substitutions directly as “name ↦ value” for both expression and binding patterns.

Apply this diff:

@@
-      case parseExpression patternStr of
-        Right expr -> do
-          let substs = matchProgram expr prog
-          logDebug (printf "Found %d matches for expression pattern" (length substs))
-          filtered <- filterByCondition' maybeCond substs prog
-          logDebug (printf "After filtering: %d matches" (length filtered))
-          let exprs = extractMatchedExpressions expr filtered
-          formatMatches exprs mode
+      case parseExpression patternStr of
+        Right expr -> do
+          let substs = matchProgram expr prog
+          logDebug (printf "Found %d matches for expression pattern" (length substs))
+          filtered <- filterByCondition' maybeCond substs prog
+          logDebug (printf "After filtering: %d matches" (length filtered))
+          formatSubsts filtered mode
         Left _ -> case parseBinding patternStr of
           Right binding -> do
             let substs = findBindingMatches binding rootExpr
             logDebug (printf "Found %d matches for binding pattern" (length substs))
-            filtered <- filterByCondition' maybeCond (map snd substs) prog
+            filtered <- filterByCondition' maybeCond (map snd substs) prog
             logDebug (printf "After filtering: %d matches" (length filtered))
-            let matchedBindings = [bd | (bd, s) <- substs, s `elem` filtered]
-            formatBindingMatches matchedBindings mode
+            formatSubsts filtered mode
           Left err -> throwIO $ userError $ "Invalid pattern: " ++ err
@@
-    extractMatchedExpressions :: Expression -> [Subst] -> [Expression]
-    extractMatchedExpressions pattern substs = 
-      [expr | Subst m <- substs,
-              MvExpression expr _ <- Map.elems m]
+    formatSubsts :: [Subst] -> PrintMode -> IO String
+    formatSubsts [] _ = pure ""
+    formatSubsts substs mode = do
+      let fmtExpr e = case mode of
+                        SWEET -> prettyExpression' e
+                        SALTY -> prettyExpression e
+      let lineOf (name, MvExpression e _) = Just (show name <> " ↦ " <> fmtExpr e)
+          lineOf (name, MvBinding b _)    = Just (show name <> " ↦ " <> prettyBinding b)
+          lineOf _                        = Nothing
+      let lines' =
+            [ ln
+            | Subst m <- substs
+            , (name, mv) <- Map.toList m
+            , Just ln <- [lineOf (name, mv)]
+            ]
+      pure (unlines lines')
@@
-    formatMatches :: [Expression] -> PrintMode -> IO String
-    formatMatches [] _ = pure ""
-    formatMatches exprs mode = do
-      let formatted = map (\e -> case mode of
-                                  SWEET -> prettyExpression' e
-                                  SALTY -> prettyExpression e) exprs
-      pure $ unlines formatted
-    formatBindingMatches :: [Binding] -> PrintMode -> IO String
-    formatBindingMatches [] _ = pure ""
-    formatBindingMatches bds mode = do
-      let formatted = map prettyBinding bds
-      pure $ unlines formatted

Note: This assumes the meta-name key in Subst has a sensible Show instance (e.g., renders “!a” as “a”). If not, switch to the appropriate pretty-printer for meta names.


400-414: Deep-binding traversal may miss cases; reuse Matcher’s deep helpers or broaden recursion.

Manual matching only visits ExFormation, ExDispatch, ExApplication and recurses from BiTau. Bindings under other nodes could be skipped now or in the future.

I can replace this with a helper that walks every binding occurrence or reuse a deep match from Matcher if available. To confirm coverage, run:

#!/bin/bash
# Inspect constructors to ensure traversal covers all cases
rg -nP -C2 'data\s+Expression\b|data\s+Binding\b' src

# Check Matcher for deep binding helpers you can reuse
rg -nP -C2 '\bmatch.*Binding.*Deep\b|\bmatchBindingDeep\b|\bmatch.*Deep\b' src

If a deep-binding matcher exists, wire it here; otherwise, I’ll draft a uniform traversal visiting all constructors that can contain bindings.

🧹 Nitpick comments (3)
test/CLISpec.hs (3)

146-149: Consider asserting the meta-name mapping for expression patterns, not only values.

PR goals mention mappings like “t ↦ ξ.k”. For expression patterns (e.g., "!e.c"), asserting “e ↦ ξ.b” would make output semantics consistent across pattern types. Current tests will still pass if CLI prints “e ↦ ξ.b” because you check substrings, but consider tightening the expectation.

Would you like me to update this test to also assert the meta name (e.g., include "e ↦ ξ.b")?


199-203: The --when test should cover a positive case too.

Right now it only validates “filters to empty.” Add a case that keeps a specific binding when the condition is met to ensure integration with parseConditionString/meetCondition is exercised end‑to‑end.

I can add a passing condition test (e.g., eq matching a concrete name) if you want.


247-250: Rename for precision: this checks empty stdin, not an error.

The title “handles stdin read errors gracefully” is misleading since empty stdin is valid input and returns success. Consider “handles empty stdin gracefully.”

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81dd32e and 3c902b4.

📒 Files selected for processing (2)
  • src/CLI.hs (10 hunks)
  • test/CLISpec.hs (1 hunks)
🔇 Additional comments (3)
test/CLISpec.hs (2)

104-121: Solid, focused coverage for the new “match” command.

Good breadth across binding patterns, nested formations, xi/Φ/⊥/Δ/λ/ρ cases, and basic error handling. This will catch most regressions in the CLI surface.


171-178: Nice: file and XMIR input paths covered.

This validates IOFormat plumbing and parser selection through the CLI.

src/CLI.hs (1)

198-208: CLI parser for “match” looks correct and consistent with the existing style.

Options and help text are clear, integrate with log-level and input format as expected.

logDebug "Reading from stdin"
getContents' `catch` (\(e :: SomeException) -> throwIO (CouldNotReadFromStdin (show e)))
performMatch' :: String -> Maybe Condition -> Program -> PrintMode -> IO String
performMatch' patternStr maybeCond prog@(Program rootExpr) mode = do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yarkivaev the functionality of matching program with rule is already implemented in Rule module, the function matchProgramWithRule is what you need

@maxonfjvipon
Copy link
Member

@yarkivaev what's up with this one? If you're not planning to continue working on this PR, please close it

@maxonfjvipon
Copy link
Member

@yarkivaev reminder

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

match new command for the CLI interface is missing

2 participants