packages feed

smirk-0.1.0.0: src/Smirk/Core/Compiler.hs

{- |
Module      : Smirk.Core.Compiler
Description : AST to Bytecode compilation with jump resolution and capture group indexing
Copyright   : (c) Nadia Yvette Chambers, 2026
License     : Apache-2.0

Compiles the high-level PCRE 'AST' into a flat vector of bytecode instructions
('Opcode') ready for execution by the Pike VM. Resolves relative jump offsets,
generates nondeterministic thread branches for repetitions and alternations,
and assigns sequential 1-based indices to named and unnamed capture groups.
-}
module Smirk.Core.Compiler
  ( compileAST
  , compileToRegex
  , assignGroupIndices
  ) where

import qualified Data.Map.Strict as Map
import Data.Map.Strict (Map)
import Data.Text (Text)
import qualified Data.Vector as V

import Smirk.Core.AST
import Smirk.Core.Bytecode

-- | Compile an AST into a high-level 't:Regex' record ready for execution.
-- Wraps the program in top-level capture group 0 (@OpSaveStart 0@ / @OpSaveEnd 0@)
-- and terminates with @OpMatch@.
compileToRegex :: Text -> AST -> Regex
compileToRegex patternStr rawAST =
  let (numberedAST, groupMap, maxIdx) = assignGroupIndices rawAST
      progOps = OpSaveStart 0 : compileAST numberedAST ++ [OpSaveEnd 0, OpMatch]
  in Regex
      { originalPattern = patternStr
      , program         = V.fromList progOps
      , groupNames      = groupMap
      , maxGroupIndex   = maxIdx
      }

-- | Sequentially assign 1-based capture group indices in left-to-right order.
-- Returns the modified AST with assigned group indices, the name-to-index map,
-- and the maximum allocated index.
assignGroupIndices :: AST -> (AST, Map Text Int, Int)
assignGroupIndices rootAST =
  let (ast', (finalIdx, groupMap)) = go rootAST (1, Map.empty)
  in (ast', groupMap, finalIdx - 1)
  where
    go :: AST -> (Int, Map Text Int) -> (AST, (Int, Map Text Int))
    go ast (nextIdx, m) = case ast of
      Group mName _ inner ->
        let thisIdx = nextIdx
            m' = case mName of
              Just n  -> Map.insert n thisIdx m
              Nothing -> m
            (inner', st') = go inner (thisIdx + 1, m')
        in (Group mName (Just thisIdx) inner', st')
      Seq items ->
        let (items', st') = foldr (\item (accItems, accSt) ->
                                    let (item', accSt') = go item accSt
                                    in (item' : accItems, accSt')
                                  ) ([], (nextIdx, m)) (reverse items)
        in (Seq (reverse items'), st')
      Alt branches ->
        let (branches', st') = foldr (\b (accB, accSt) ->
                                       let (b', accSt') = go b accSt
                                       in (b' : accB, accSt')
                                     ) ([], (nextIdx, m)) (reverse branches)
        in (Alt (reverse branches'), st')
      Repeat minN mMax kind inner ->
        let (inner', st') = go inner (nextIdx, m)
        in (Repeat minN mMax kind inner', st')
      NonCapturing inner ->
        let (inner', st') = go inner (nextIdx, m)
        in (NonCapturing inner', st')
      Lookaround dir isPos inner ->
        let (inner', st') = go inner (nextIdx, m)
        in (Lookaround dir isPos inner', st')
      other -> (other, (nextIdx, m))

-- | Compile an AST node to a flat list of opcodes with relative jump resolution.
compileAST :: AST -> [Opcode]
compileAST = \case
  Empty -> []
  Lit c -> [OpChar c]
  Class cls -> [OpClass cls]
  AnchorAssert a -> [OpAnchor a]
  Seq items -> concatMap compileAST items
  Alt branches -> compileAlt branches
  Repeat minN mMax kind inner -> compileRepeat minN mMax kind inner
  Group _ (Just idx) inner -> OpSaveStart idx : compileAST inner ++ [OpSaveEnd idx]
  Group _ Nothing inner -> compileAST inner
  NonCapturing inner -> compileAST inner
  Lookaround Lookahead isPos inner ->
    let subProg = V.fromList (compileAST inner ++ [OpMatch])
    in [OpAssertLookahead subProg isPos]
  Lookaround Lookbehind isPos inner ->
    let subProg = V.fromList (compileAST inner ++ [OpMatch])
    in [OpAssertLookbehind subProg isPos]
  BackRef idx -> [OpMatchBackRef idx]
  NamedBackRef _ -> []

compileAlt :: [AST] -> [Opcode]
compileAlt [] = []
compileAlt [singleBranch] = compileAST singleBranch
compileAlt (b1 : bs) =
  let code1 = compileAST b1
      code2 = compileAlt bs
      len1 = length code1
      len2 = length code2
      splitOp = OpSplit 1 (1 + len1 + 1)
      jumpToEnd = OpJump (len2 + 1)
  in splitOp : code1 ++ jumpToEnd : code2

compileRepeat :: Int -> Maybe Int -> RepKind -> AST -> [Opcode]
compileRepeat 0 (Just 1) RepGreedy inner =
  let body = compileAST inner
      len = length body
  in OpSplit 1 (len + 1) : body
compileRepeat 0 (Just 1) RepLazy inner =
  let body = compileAST inner
      len = length body
  in OpSplit (len + 1) 1 : body
compileRepeat 0 Nothing RepGreedy inner =
  let body = compileAST inner
      len = length body
      loop = OpSplit 1 (len + 2) : body ++ [OpJump (- (len + 1))]
  in loop
compileRepeat 0 Nothing RepLazy inner =
  let body = compileAST inner
      len = length body
      loop = OpSplit (len + 2) 1 : body ++ [OpJump (- (len + 1))]
  in loop
compileRepeat 1 Nothing RepGreedy inner =
  let body = compileAST inner
      len = length body
  in body ++ [OpSplit (- len) 1]
compileRepeat 1 Nothing RepLazy inner =
  let body = compileAST inner
      len = length body
  in body ++ [OpSplit 1 (- len)]
compileRepeat minN mMax kind inner =
  let exact = concat (replicate minN (compileAST inner))
      extra = case mMax of
        Nothing -> compileRepeat 0 Nothing kind inner
        Just maxN | maxN > minN ->
          let remN = maxN - minN
              single = compileRepeat 0 (Just 1) kind inner
          in concat (replicate remN single)
        _ -> []
  in exact ++ extra