canontra-0.1.0.0: test/Canontra/SymbolTableSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : Canontra.SymbolTableSpec
Description : Test suite for zero-allocation SymbolTable and Symbol interning engine.
-}
module Canontra.SymbolTableSpec (spec) where
import Control.DeepSeq (deepseq)
import Data.Binary (decode, encode)
import qualified Data.ByteString.Char8 as BSC
import qualified Data.Map.Strict as Map
import qualified Data.Vector as V
import Test.Hspec
import Test.QuickCheck
import Canontra.Parser.SymbolTable
spec :: Spec
spec = do
describe "SymbolTable Interning Engine" $ do
it "initializes an empty table with 0 entries" $ do
symbolTableSize emptySymbolTable `shouldBe` 0
stLookup emptySymbolTable `shouldBe` Map.empty
stReverse emptySymbolTable `shouldBe` V.empty
it "interns a single ByteString symbol and resolves it" $ do
let (sid, st) = internSymbolBS "variable_name" emptySymbolTable
sid `shouldBe` SymbolId 0
symbolTableSize st `shouldBe` 1
resolveSymbolBS sid st `shouldBe` Just "variable_name"
resolveSymbolText sid st `shouldBe` Just "variable_name"
lookupSymbolBS "variable_name" st `shouldBe` Just (SymbolId 0)
it "interns a Text symbol and resolves it" $ do
let (sid, st) = internSymbolText "myFunctionName" emptySymbolTable
sid `shouldBe` SymbolId 0
symbolTableSize st `shouldBe` 1
resolveSymbolText sid st `shouldBe` Just "myFunctionName"
lookupSymbolText "myFunctionName" st `shouldBe` Just (SymbolId 0)
it "preserves idempotence: interning the same symbol returns the identical SymbolId" $ do
let (sid1, st1) = internSymbolBS "alpha" emptySymbolTable
(sid2, st2) = internSymbolBS "alpha" st1
(sid3, st3) = internSymbolBS "alpha" st2
sid1 `shouldBe` SymbolId 0
sid2 `shouldBe` SymbolId 0
sid3 `shouldBe` SymbolId 0
symbolTableSize st3 `shouldBe` 1
it "assigns strictly monotonic and distinct SymbolIds to different symbols" $ do
let (sidA, st1) = internSymbolBS "foo" emptySymbolTable
(sidB, st2) = internSymbolBS "bar" st1
(sidC, st3) = internSymbolBS "baz" st2
sidA `shouldBe` SymbolId 0
sidB `shouldBe` SymbolId 1
sidC `shouldBe` SymbolId 2
symbolTableSize st3 `shouldBe` 3
resolveSymbolBS sidA st3 `shouldBe` Just "foo"
resolveSymbolBS sidB st3 `shouldBe` Just "bar"
resolveSymbolBS sidC st3 `shouldBe` Just "baz"
it "correctly performs batch interning with internManyBS" $ do
let symbols = ["apple", "banana", "cherry", "apple", "banana", "date"]
(sids, st) = internManyBS symbols emptySymbolTable
length sids `shouldBe` 6
symbolTableSize st `shouldBe` 4
sids `shouldBe` [SymbolId 0, SymbolId 1, SymbolId 2, SymbolId 0, SymbolId 1, SymbolId 3]
it "correctly performs batch interning with fromListText" $ do
let texts = ["fn", "let", "mut", "fn", "let"]
(st, sids) = fromListText texts
symbolTableSize st `shouldBe` 3
sids `shouldBe` [SymbolId 0, SymbolId 1, SymbolId 2, SymbolId 0, SymbolId 1]
resolveSymbolText (SymbolId 0) st `shouldBe` Just "fn"
resolveSymbolText (SymbolId 1) st `shouldBe` Just "let"
resolveSymbolText (SymbolId 2) st `shouldBe` Just "mut"
it "returns Nothing when looking up non-existent symbols" $ do
let (_, st) = internSymbolBS "existing" emptySymbolTable
lookupSymbolBS "missing" st `shouldBe` Nothing
lookupSymbolText "missing" st `shouldBe` Nothing
resolveSymbolBS (SymbolId 999) st `shouldBe` Nothing
resolveSymbolText (SymbolId 999) st `shouldBe` Nothing
it "extracts all entries in order via symbolTableEntries" $ do
let symbols = ["alpha", "beta", "gamma"]
(st, _) = fromListBS symbols
entries = symbolTableEntries st
entries `shouldBe` [(SymbolId 0, "alpha"), (SymbolId 1, "beta"), (SymbolId 2, "gamma")]
it "preloads polyglot keywords for Python, JS/TS, Go, and Rust" $ do
let st = preloadPolyglotKeywords
symbolTableSize st `shouldSatisfy` (> 50)
lookupSymbolBS "def" st `shouldSatisfy` (/= Nothing)
lookupSymbolBS "async" st `shouldSatisfy` (/= Nothing)
lookupSymbolBS "func" st `shouldSatisfy` (/= Nothing)
lookupSymbolBS "struct" st `shouldSatisfy` (/= Nothing)
lookupSymbolBS "trait" st `shouldSatisfy` (/= Nothing)
lookupSymbolBS "interface" st `shouldSatisfy` (/= Nothing)
it "binary serializes and deserializes SymbolTable losslessly" $ do
let symbols = ["module", "import", "class", "def", "return"]
(st, _) = fromListBS symbols
encoded = encode st
decoded = decode encoded :: SymbolTable
decoded `shouldBe` st
symbolTableSize decoded `shouldBe` 5
resolveSymbolBS (SymbolId 3) decoded `shouldBe` Just "def"
it "evaluates strictly with deepseq without leaking thunks" $ do
let symbols = ["x" <> BSC.pack (show i) | i <- [1..500 :: Int]]
(st, sids) = fromListBS symbols
deepseq st () `shouldBe` ()
deepseq sids () `shouldBe` ()
describe "Property-Based QuickCheck Tests" $ do
it "Property: Resolution bijection for arbitrary ASCII token sequences" $
property $ \strs ->
let cleanStrs = filter (not . null) (strs :: [String])
bsList = map BSC.pack cleanStrs
(st, sids) = fromListBS bsList
in all (\(b, sid) -> resolveSymbolBS sid st == Just b) (zip bsList sids)
it "Property: Table size is equal to number of unique symbols" $
property $ \strs ->
let bsList = map BSC.pack (strs :: [String])
(st, _) = fromListBS bsList
uniqueCount = length (Map.keys (Map.fromList (map (, ()) bsList)))
in symbolTableSize st == uniqueCount