diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@
     , "y" =: [2 * x + 1 | x <- [1, 2, 3, 4, 5, 6]] :: [Double]
     ]
 
-model = fit defaultLinearConfig (D.col @Double "y") sales
+model = fit defaultLinearConfig (D.double "y") sales
 putStrLn (D.prettyPrint (predict model))
 ```
 
@@ -115,7 +115,7 @@
     , "species"      =: [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2] :: [Double]
     ]
 
-tree = fit defaultTreeConfig (D.col @Double "species") flowers
+tree = fit defaultTreeConfig (D.double "species") flowers
 putStrLn (D.prettyPrint (predict tree))
 ```
 
@@ -180,7 +180,7 @@
 
 sr = fit
         defaultSRConfig { srSeed = 3, srGenerations = 50, srPopSize = 300, srUnaryOps = [] }
-        (D.col @Double "y") curve
+        (D.double "y") curve
 putStrLn (D.prettyPrint (srBest sr) ++ "   (mse " ++ show (srBestMSE sr) ++ ")")
 ```
 
@@ -198,7 +198,6 @@
 > <!-- scripths:mime text/plain -->
 > <interactive>:87:47: error: [GHC-88464]
 >     Variable not in scope: model
->     Suggested fix: Perhaps use ‘T.mode’ (imported from DataFrame.Typed)
 > 
 > <interactive>:87:54: error: [GHC-88464]
 >     Variable not in scope: sales :: D.DataFrame
@@ -215,7 +214,7 @@
 ```haskell
 scaler      = standardScaler ["x"] sales
 scaledSales = applyTransform (scalerTransform scaler) sales
-scaledModel = fit defaultLinearConfig (D.col @Double "y") scaledSales
+scaledModel = fit defaultLinearConfig (D.double "y") scaledSales
 
 deployed = compileThrough (scalerTransform scaler) (predict scaledModel)
 putStr (unlines
@@ -242,14 +241,14 @@
 original frame with no preprocessing step at inference time.
 
 ```haskell
-evaluate rmse deployed (D.col @Double "y") sales
+evaluate rmse deployed (D.double "y") sales
 ```
 
 > <!-- scripths:mime text/plain -->
 > <interactive>:112:15: error: [GHC-88464]
 >     Variable not in scope: deployed :: D.Expr Double
 > 
-> <interactive>:112:44: error: [GHC-88464]
+> <interactive>:112:39: error: [GHC-88464]
 >     Variable not in scope: sales :: D.DataFrame
 
 ## Splitting the data, and evaluation
@@ -279,10 +278,10 @@
 import System.Random (mkStdGen)
 
 (train, test) = D.randomSplit (mkStdGen 7) 0.75 clean
-heldModel     = fit defaultLinearConfig (D.col @Double "y") train
+heldModel     = fit defaultLinearConfig (D.double "y") train
 putStr (unlines
-    [ "held-out R^2:  " ++ show (evaluate r2   (predict heldModel) (D.col @Double "y") test)
-    , "held-out RMSE: " ++ show (evaluate rmse (predict heldModel) (D.col @Double "y") test) ])
+    [ "held-out R^2:  " ++ show (evaluate r2   (predict heldModel) (D.double "y") test)
+    , "held-out RMSE: " ++ show (evaluate rmse (predict heldModel) (D.double "y") test) ])
 ```
 
 > <!-- scripths:mime text/plain -->
@@ -294,8 +293,8 @@
 fold. You pass a `train -> Expr` closure, so it works with any model:
 
 ```haskell
-cv = crossValidate 5 0 rmse (D.col @Double "y")
-         (\tr -> predict (fit defaultLinearConfig (D.col @Double "y") tr))
+cv = crossValidate 5 0 rmse (D.double "y")
+         (\tr -> predict (fit defaultLinearConfig (D.double "y") tr))
          clean
 putStrLn ("5-fold RMSE: " ++ show (sum cv / fromIntegral (length cv)))
 ```
@@ -313,17 +312,17 @@
 macro/weighted averages):
 
 ```haskell
-clf = fit defaultLogisticConfig (D.col @Double "species") flowers
-putStr (show (classificationReportExpr (predict clf) (D.col @Double "species") flowers))
+clf = fit defaultLogisticConfig (D.double "species") flowers
+putStr (show (classificationReportExpr (predict clf) (D.double "species") flowers))
 ```
 
 > <!-- scripths:mime text/plain -->
-> <interactive>:170:59: error: [GHC-88464]
+> <interactive>:170:54: error: [GHC-88464]
 >     Variable not in scope: flowers :: D.DataFrame
 > 
 > <interactive>:173:49: error: [GHC-88464] Variable not in scope: clf
 > 
-> <interactive>:173:80: error: [GHC-88464]
+> <interactive>:173:75: error: [GHC-88464]
 >     Variable not in scope: flowers :: D.DataFrame
 
 ## Pipelines compose as a monoid
@@ -335,7 +334,7 @@
 ```haskell
 features = ["petal_length", "petal_width"]
 scalerF  = standardScaler features flowers
-pca      = fit (PCAConfig (NComp 2) True) (map (D.col @Double) features) flowers
+pca      = fit (PCAConfig (NComp 2) True) (map (D.double) features) flowers
 pipeline = scalerTransform scalerF <> pcaTransform pca
 
 D.columnNames (applyTransform pipeline flowers)
@@ -345,7 +344,7 @@
 > <interactive>:182:36: error: [GHC-88464]
 >     Variable not in scope: flowers :: D.DataFrame
 > 
-> <interactive>:183:74: error: [GHC-88464]
+> <interactive>:183:69: error: [GHC-88464]
 >     Variable not in scope: flowers :: D.DataFrame
 > 
 > <interactive>:188:31: error: [GHC-88464]
@@ -373,17 +372,17 @@
     as = [-1, -1, 1, 1, -2, 2, -2, 2] :: [Double]
     bs = [-1, 1, -1, 1, -2, -2, 2, 2] :: [Double]
 
-rawModel = fit defaultLinearConfig (D.col @Double "y") interactions
-feature  = fit defaultSynthesisConfig (D.col @Double "y") interactions
+rawModel = fit defaultLinearConfig (D.double "y") interactions
+feature  = fit defaultSynthesisConfig (D.double "y") interactions
 withFeat = D.derive "synth" (predict feature) interactions
 fitModel =
-    fit defaultLinearConfig (D.col @Double "y")
+    fit defaultLinearConfig (D.double "y")
         (D.select ["synth", "y"] withFeat)
 
 putStr (unlines
     [ "discovered feature: " ++ D.prettyPrint (predict feature)
-    , "raw linear R^2:     " ++ show (evaluate r2 (predict rawModel) (D.col @Double "y") interactions)
-    , "with synth feature: " ++ show (evaluate r2 (predict fitModel) (D.col @Double "y") withFeat)
+    , "raw linear R^2:     " ++ show (evaluate r2 (predict rawModel) (D.double "y") interactions)
+    , "with synth feature: " ++ show (evaluate r2 (predict fitModel) (D.double "y") withFeat)
     ])
 ```
 
diff --git a/dataframe-learn.cabal b/dataframe-learn.cabal
--- a/dataframe-learn.cabal
+++ b/dataframe-learn.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.4
 name:               dataframe-learn
-version:            2.5.0.0
+version:            2.5.0.1
 synopsis:           Interpretable, expression-returning machine learning for the dataframe ecosystem.
 description:
     A small scikit-learn-style ML library where every model returns both an
@@ -62,8 +62,8 @@
                         containers >= 0.6.7 && < 0.10,
                         parallel >= 3.3 && < 4,
                         random >= 1.2 && < 2,
-                        dataframe-core >= 2.6 && < 2.7,
-                        dataframe-operations >= 2.6 && < 2.7,
+                        dataframe-core >= 2.7 && < 2.8,
+                        dataframe-operations >= 2.7 && < 2.8,
                         text >= 2.1 && < 3,
                         vector >= 0.13 && < 0.15,
                         vector-algorithms >= 0.9 && < 0.11
@@ -106,11 +106,11 @@
                         containers >= 0.6.7 && < 0.10,
                         parallel >= 3.3 && < 4,
                         random >= 1.2 && < 2,
-                        dataframe-core >= 2.6 && < 2.7,
-                        dataframe-core >= 2.6 && < 2.7,
-                        dataframe-operations >= 2.6 && < 2.7,
-                        dataframe-operations >= 2.6 && < 2.7,
-                        dataframe-expr-serializer >= 1.3 && < 1.4,
+                        dataframe-core >= 2.7 && < 2.8,
+                        dataframe-core >= 2.7 && < 2.8,
+                        dataframe-operations >= 2.7 && < 2.8,
+                        dataframe-operations >= 2.7 && < 2.8,
+                        dataframe-expr-serializer >= 1.4 && < 1.5,
                         dataframe-learn:internal,
                         text >= 2.1 && < 3,
                         vector >= 0.13 && < 0.15,
@@ -144,13 +144,13 @@
                         aeson >= 0.11.0.0 && < 3,
                         bytestring >= 0.11 && < 0.14,
                         containers >= 0.6.7 && < 0.10,
-                        dataframe-core >= 2.6 && < 2.7,
-                        dataframe-core >= 2.6 && < 2.7,
+                        dataframe-core >= 2.7 && < 2.8,
+                        dataframe-core >= 2.7 && < 2.8,
                         dataframe-csv >= 2.3 && < 2.4,
                         dataframe-learn,
                         dataframe-learn:internal,
-                        dataframe-operations >= 2.6 && < 2.7,
-                        dataframe-operations >= 2.6 && < 2.7,
+                        dataframe-operations >= 2.7 && < 2.8,
+                        dataframe-operations >= 2.7 && < 2.8,
                         HUnit >= 1.6 && < 1.8,
                         QuickCheck >= 2 && < 3,
                         random >= 1 && < 2,
diff --git a/src-internal/DataFrame/DecisionTree/Cart.hs b/src-internal/DataFrame/DecisionTree/Cart.hs
--- a/src-internal/DataFrame/DecisionTree/Cart.hs
+++ b/src-internal/DataFrame/DecisionTree/Cart.hs
@@ -301,7 +301,7 @@
     (Columnable b, VU.Unbox b) =>
     T.Text -> Maybe Bitmap -> VU.Vector b -> [CartFeature]
 numericFeature c Nothing v = case testEquality (typeRep @b) (typeRep @Double) of
-    Just Refl -> [CartFeature v Nothing (\t _ -> F.col @Double c .<=. F.lit t)]
+    Just Refl -> [CartFeature v Nothing (\t _ -> F.double c .<=. F.lit t)]
     Nothing -> case sIntegral @b of
         STrue ->
             [ CartFeature
@@ -327,9 +327,9 @@
     T.Text -> Maybe (b -> Double, Double -> Expr (Maybe Bool))
 nullableLeq c
     | Just Refl <- testEquality (typeRep @b) (typeRep @Double) =
-        Just (id, \t -> F.col @(Maybe Double) c .<= F.lit t)
+        Just (id, \t -> F.maybeDouble c .<= F.lit t)
     | Just Refl <- testEquality (typeRep @b) (typeRep @Int) =
-        Just (fromIntegral, \t -> F.col @(Maybe Int) c .<= F.lit t)
+        Just (fromIntegral, \t -> F.maybeInt c .<= F.lit t)
     | otherwise = Nothing
 
 oneHotFeatures ::
@@ -348,7 +348,7 @@
     CartFeature
         (VU.generate (V.length v) (\i -> if v V.! i == cat then 1 else 0))
         Nothing
-        (\_ _ -> F.col @T.Text c ./=. F.lit cat)
+        (\_ _ -> F.text c ./=. F.lit cat)
 oneHot c (Just nulls) v cat =
     CartFeature
         (VU.generate (V.length v) value)
@@ -359,7 +359,7 @@
         | nulls VU.! i = 1 / 0
         | v V.! i == cat = 1
         | otherwise = 0
-    col' = F.col @(Maybe T.Text) c
+    col' = F.maybeText c
     -- Threshold 1 passes every non-null row, so that split is null vs non-null.
     cond t side
         | t < 1 = F.fromMaybe (side == NullsLeft) (col' ./= F.lit cat)
diff --git a/src-internal/DataFrame/DecisionTree/CondVec.hs b/src-internal/DataFrame/DecisionTree/CondVec.hs
--- a/src-internal/DataFrame/DecisionTree/CondVec.hs
+++ b/src-internal/DataFrame/DecisionTree/CondVec.hs
@@ -1,5 +1,4 @@
 {-# LANGUAGE GADTs #-}
-{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
 
@@ -24,10 +23,12 @@
 import DataFrame.Internal.Column (TypedColumn (..), toVector)
 import DataFrame.Internal.DataFrame (DataFrame)
 import DataFrame.Internal.Expression (
-    BinaryOp (binaryName),
+    BinOpId (..),
+    BinaryOp (binaryId),
     Expr (..),
+    builtinOf,
     eqExpr,
-    normalize,
+    structuralKey,
  )
 import DataFrame.Internal.Interpreter (interpret)
 
@@ -57,13 +58,14 @@
 -- | Truth vectors keyed by structural form, read-only once built.
 type CondCache = M.Map T.Text (VU.Vector Bool)
 
--- | Structural key matching the candidate-dedup key.
-condCacheKey :: Expr Bool -> T.Text
-condCacheKey = T.pack . show . normalize
+-- | Structural key; Nothing for conditions with anonymous operators.
+condCacheKey :: Expr Bool -> Maybe T.Text
+condCacheKey = fmap T.pack . structuralKey
 
 -- | Seed a cache from already-materialized candidate 'CondVec's (no interpret).
 condCacheFromVecs :: [CondVec] -> CondCache
-condCacheFromVecs cvs = M.fromList [(condCacheKey (cvExpr cv), cvVec cv) | cv <- cvs]
+condCacheFromVecs cvs =
+    M.fromList [(k, cvVec cv) | cv <- cvs, Just k <- [condCacheKey (cvExpr cv)]]
 
 -- | Add a tree's branch-condition vectors to a cache.
 addTreeCondsToCache :: DataFrame -> Tree a -> CondCache -> CondCache
@@ -73,16 +75,16 @@
     go (Branch cond l r) c = go r (go l (insertCond df cond c))
 
 insertCond :: DataFrame -> Expr Bool -> CondCache -> CondCache
-insertCond df cond c
-    | M.member k c = c
-    | otherwise =
-        maybe c (\cv -> M.insert k (cvVec cv) c) (materializeCondVec df cond)
-  where
-    k = condCacheKey cond
+insertCond df cond c = case condCacheKey cond of
+    Nothing -> c
+    Just k
+        | M.member k c -> c
+        | otherwise ->
+            maybe c (\cv -> M.insert k (cvVec cv) c) (materializeCondVec df cond)
 
 -- | A condition's truth vector: a cache hit, else interpret.
 lookupCondVec :: CondCache -> DataFrame -> Expr Bool -> Maybe (VU.Vector Bool)
-lookupCondVec cache df cond = case M.lookup (condCacheKey cond) cache of
+lookupCondVec cache df cond = case (`M.lookup` cache) =<< condCacheKey cond of
     hit@(Just _) -> hit
     Nothing -> cvVec <$> materializeCondVec df cond
 
@@ -99,7 +101,7 @@
 -- | A same-column same-direction Double threshold comparison.
 data ThreshCmp = ThreshCmp
     { tcCol :: !T.Text
-    , tcName :: !T.Text
+    , tcOp :: !BinOpId
     , tcThr :: !Double
     , tcRebuild :: Double -> Expr Bool
     }
@@ -108,29 +110,31 @@
 asDoubleThreshold (Binary op (Col c :: Expr cc) (Lit (t :: tt))) =
     case ( testEquality (typeRep @cc) (typeRep @Double)
          , testEquality (typeRep @tt) (typeRep @Double)
+         , builtinOf (binaryId op)
          ) of
-        (Just Refl, Just Refl) -> Just (ThreshCmp c (binaryName op) t (Binary op (Col c) . Lit))
+        (Just Refl, Just Refl, Just tag) ->
+            Just (ThreshCmp c tag t (Binary op (Col c) . Lit))
         _ -> Nothing
 asDoubleThreshold _ = Nothing
 
-directionalNames :: [T.Text]
-directionalNames = ["lt", "leq", "gt", "geq"]
+directionalOps :: [BinOpId]
+directionalOps = [BLt, BLeq, BGt, BGeq]
 
 -- | Tighter (AND) or looser (OR) of two same-direction thresholds.
-chooseThreshold :: Bool -> T.Text -> Double -> Double -> Double
-chooseThreshold isAnd name t1 t2
+chooseThreshold :: Bool -> BinOpId -> Double -> Double -> Double
+chooseThreshold isAnd op t1 t2
     | leftDir = if isAnd then min t1 t2 else max t1 t2
     | otherwise = if isAnd then max t1 t2 else min t1 t2
   where
-    leftDir = name == "lt" || name == "leq"
+    leftDir = op == BLt || op == BLeq
 
 -- | Collapse two same-direction Double comparisons into one, if possible.
 consolidateThreshold :: Bool -> Expr Bool -> Expr Bool -> Maybe (Expr Bool)
 consolidateThreshold isAnd ea eb = do
     a <- asDoubleThreshold ea
     b <- asDoubleThreshold eb
-    if tcCol a == tcCol b && tcName a == tcName b && tcName a `elem` directionalNames
-        then Just (tcRebuild a (chooseThreshold isAnd (tcName a) (tcThr a) (tcThr b)))
+    if tcCol a == tcCol b && tcOp a == tcOp b && tcOp a `elem` directionalOps
+        then Just (tcRebuild a (chooseThreshold isAnd (tcOp a) (tcThr a) (tcThr b)))
         else Nothing
 
 -- | AND-combine two cached conditions, consolidating thresholds if possible.
diff --git a/src-internal/DataFrame/DecisionTree/Fit.hs b/src-internal/DataFrame/DecisionTree/Fit.hs
--- a/src-internal/DataFrame/DecisionTree/Fit.hs
+++ b/src-internal/DataFrame/DecisionTree/Fit.hs
@@ -44,7 +44,8 @@
 import DataFrame.Internal.Expression (Expr (..))
 import DataFrame.Internal.Interpreter (interpret)
 import DataFrame.Operations.Core (nRows)
-import DataFrame.Operations.Subset (exclude, filterWhere)
+import DataFrame.Operations.Subset (exclude)
+import qualified DataFrame.Operations.Subset as Subset
 
 import Control.Exception (throw)
 import Data.Function (on)
@@ -101,7 +102,7 @@
 pruneTree = pruneExpr
 
 partitionDataFrame :: Expr Bool -> DataFrame -> (DataFrame, DataFrame)
-partitionDataFrame cond df = (filterWhere cond df, filterWhere (F.not cond) df)
+partitionDataFrame cond df = (Subset.filter cond df, Subset.filter (F.not cond) df)
 
 -- | Laplace-smoothed Gini impurity of the target distribution.
 calculateGini ::
diff --git a/src-internal/DataFrame/DecisionTree/Pool.hs b/src-internal/DataFrame/DecisionTree/Pool.hs
--- a/src-internal/DataFrame/DecisionTree/Pool.hs
+++ b/src-internal/DataFrame/DecisionTree/Pool.hs
@@ -36,7 +36,7 @@
     eSize,
     eqExpr,
     getColumns,
-    normalize,
+    structuralKey,
  )
 
 import Control.Parallel.Strategies (parListChunk, rdeepseq, using)
@@ -157,12 +157,15 @@
 admitKeys = go []
   where
     go acc seen [] = (reverse acc, seen)
-    go acc !seen (c : cs)
-        | structuralKey c `Set.member` seen = go acc seen cs
-        | otherwise = go (c : acc) (Set.insert (structuralKey c) seen) cs
+    go acc !seen (c : cs) = case cvKey c of
+        Nothing -> go (c : acc) seen cs
+        Just k
+            | k `Set.member` seen -> go acc seen cs
+            | otherwise -> go (c : acc) (Set.insert k seen) cs
 
-structuralKey :: CondVec -> String
-structuralKey = show . normalize . cvExpr
+-- | Nothing for anonymous operators, which are never merged.
+cvKey :: CondVec -> Maybe String
+cvKey = structuralKey . cvExpr
 
 -- | Admit candidates by distinct truth vector, keeping the smallest expression.
 admitVecs ::
@@ -187,17 +190,20 @@
 dedupCVByExpr = go Set.empty
   where
     go _ [] = []
-    go seen (cv : cvs)
-        | structuralKey cv `Set.member` seen = go seen cvs
-        | otherwise = cv : go (Set.insert (structuralKey cv) seen) cvs
+    go seen (cv : cvs) = case cvKey cv of
+        Nothing -> cv : go seen cvs
+        Just k
+            | k `Set.member` seen -> go seen cvs
+            | otherwise -> cv : go (Set.insert k seen) cvs
 
 -- | Deduplicate expressions by normalized form, keeping the first.
 nubByExpr :: [Expr Bool] -> [Expr Bool]
 nubByExpr = go Set.empty
   where
+    go :: Set.Set String -> [Expr Bool] -> [Expr Bool]
     go _ [] = []
-    go seen (e : es)
-        | k `Set.member` seen = go seen es
-        | otherwise = e : go (Set.insert k seen) es
-      where
-        k = show (normalize e)
+    go seen (e : es) = case structuralKey e of
+        Nothing -> e : go seen es
+        Just k
+            | k `Set.member` seen -> go seen es
+            | otherwise -> e : go (Set.insert k seen) es
diff --git a/src-internal/DataFrame/Featurize/Internal.hs b/src-internal/DataFrame/Featurize/Internal.hs
--- a/src-internal/DataFrame/Featurize/Internal.hs
+++ b/src-internal/DataFrame/Featurize/Internal.hs
@@ -80,7 +80,7 @@
             then throw (nullableInputError name (columnTypeString c))
             else
                 if D.hasElemType @Double c
-                    then fromRight undefined (columnAsUnboxedVector (F.col @Double name) df)
+                    then fromRight undefined (columnAsUnboxedVector (F.double name) df)
                     else
                         throw
                             ( asDoubleInputError
diff --git a/src/DataFrame/DBSCAN.hs b/src/DataFrame/DBSCAN.hs
--- a/src/DataFrame/DBSCAN.hs
+++ b/src/DataFrame/DBSCAN.hs
@@ -2,7 +2,6 @@
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE TypeFamilies #-}
 
 -- | Density-based clustering (DBSCAN) with brute-force region queries.
@@ -103,7 +102,7 @@
 dbscanSurrogateExpr ::
     TreeConfig -> [Expr Double] -> DBSCANModel -> DataFrame -> Expr Int
 dbscanSurrogateExpr cfg features model df =
-    fitDecisionTree cfg (F.col @Int clusterCol) augmented
+    fitDecisionTree cfg (F.int clusterCol) augmented
   where
     clusterCol = "__cluster__"
     cols = map (\e -> (columnExprName e, materializeColumn df e)) features
diff --git a/src/DataFrame/Transform.hs b/src/DataFrame/Transform.hs
--- a/src/DataFrame/Transform.hs
+++ b/src/DataFrame/Transform.hs
@@ -1,6 +1,5 @@
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
 
 -- | Fitted column transforms as a composable monoid.
 module DataFrame.Transform (
@@ -66,7 +65,7 @@
     ScalerModel (V.fromList names) (VU.fromList means) (VU.fromList stds)
   where
     cols = map column names
-    column n = case columnAsDoubleVector (F.col @Double n) df of
+    column n = case columnAsDoubleVector (F.double n) df of
         Right v -> v
         Left e -> throw e
     means = [VU.sum c / fromIntegral (max 1 (VU.length c)) | c <- cols]
@@ -84,7 +83,7 @@
 scalerTransform :: ScalerModel -> Transform
 scalerTransform m =
     Transform
-        [ (n, SomeExpr ((F.col @Double n .-. F.lit mu) ./. F.lit sigma))
+        [ (n, SomeExpr ((F.double n .-. F.lit mu) ./. F.lit sigma))
         | (n, mu, sigma) <-
             zip3
                 (V.toList (smColumns m))
diff --git a/tests-internal/DecisionTree.hs b/tests-internal/DecisionTree.hs
--- a/tests-internal/DecisionTree.hs
+++ b/tests-internal/DecisionTree.hs
@@ -91,7 +91,7 @@
 
 -- x <= 2.5: True for idx 0,1 (→ left); False for idx 2,3 (→ right)
 splitCond :: Expr Bool
-splitCond = F.col @Double "x" .<= F.lit (2.5 :: Double)
+splitCond = F.double "x" .<= F.lit (2.5 :: Double)
 
 -- Pre-computed care points for the full fixture
 carePoints3 :: [CarePoint]
@@ -216,14 +216,14 @@
 countErrorsAllCorrect :: Test
 countErrorsAllCorrect = TestCase $ do
     let cps = [CarePoint 0 GoLeft, CarePoint 1 GoRight]
-        cond = F.col @Double "x" .<= F.lit (1.5 :: Double)
+        cond = F.double "x" .<= F.lit (1.5 :: Double)
         errs = countCarePointErrors cond fixtureDF cps
     assertEqual "condition routes all care points correctly" 0 errs
 
 countErrorsAllWrong :: Test
 countErrorsAllWrong = TestCase $ do
     let cps = [CarePoint 0 GoLeft, CarePoint 1 GoRight]
-        cond = F.col @Double "x" .> F.lit (1.5 :: Double)
+        cond = F.double "x" .> F.lit (1.5 :: Double)
         errs = countCarePointErrors cond fixtureDF cps
     assertEqual "reversed condition misroutes all care points" 2 errs
 
@@ -260,8 +260,8 @@
 -- Candidate conditions that bracket the decision boundary
 sepConds :: [Expr Bool]
 sepConds =
-    [ F.col @Double "x" .<= F.lit (10.5 :: Double)
-    , F.col @Double "x" .> F.lit (10.5 :: Double)
+    [ F.double "x" .<= F.lit (10.5 :: Double)
+    , F.double "x" .> F.lit (10.5 :: Double)
     ]
 
 testCfg :: TreeConfig
@@ -276,7 +276,7 @@
 wrongStump :: Tree T.Text
 wrongStump =
     Branch
-        (F.col @Double "x" .> F.lit (10.5 :: Double))
+        (F.double "x" .> F.lit (10.5 :: Double))
         (Leaf "pos")
         (Leaf "neg")
 
@@ -324,7 +324,7 @@
 
 taoDeadBranchNoCrash :: Test
 taoDeadBranchNoCrash = TestCase $ do
-    let badCond = F.col @Double "x" .<= F.lit (0.5 :: Double)
+    let badCond = F.double "x" .<= F.lit (0.5 :: Double)
         indices = V.enumFromN 0 20
         initTree = Branch badCond (Leaf "pos") (Leaf "neg") :: Tree T.Text
         result =
@@ -348,20 +348,20 @@
 taoRecoversSingleObliqueDerived = TestCase $ do
     let labelExpr =
             F.ifThenElse
-                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))
+                ((F.double "x" + F.double "y") .<= F.lit (4.5 :: Double))
                 (F.lit ("pos" :: T.Text))
                 (F.lit ("neg" :: T.Text))
         df = D.derive @T.Text "label" labelExpr gridBaseDF
         indices = V.enumFromN 0 16
         initTree =
             Branch
-                (F.col @Double "x" .<= F.lit (2.5 :: Double))
+                (F.double "x" .<= F.lit (2.5 :: Double))
                 (Leaf "pos")
                 (Leaf "neg") ::
                 Tree T.Text
         conds =
-            [ (F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double)
-            , (F.col @Double "x" + F.col @Double "y") .> F.lit (4.5 :: Double)
+            [ (F.double "x" + F.double "y") .<= F.lit (4.5 :: Double)
+            , (F.double "x" + F.double "y") .> F.lit (4.5 :: Double)
             ]
         cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}
         result = taoOptimize @T.Text cfg "label" conds df indices initTree
@@ -375,10 +375,10 @@
 taoRecoversNestedObliqueDerived = TestCase $ do
     let labelExpr =
             F.ifThenElse
-                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))
+                ((F.double "x" + F.double "y") .<= F.lit (4.5 :: Double))
                 (F.lit ("low" :: T.Text))
                 ( F.ifThenElse
-                    ((F.col @Double "x" - F.col @Double "y") .<= F.lit (0.5 :: Double))
+                    ((F.double "x" - F.double "y") .<= F.lit (0.5 :: Double))
                     (F.lit "mid")
                     (F.lit "high")
                 )
@@ -386,19 +386,19 @@
         indices = V.enumFromN 0 16
         initTree =
             Branch
-                (F.col @Double "x" .<= F.lit (1.5 :: Double))
+                (F.double "x" .<= F.lit (1.5 :: Double))
                 (Leaf "low")
                 ( Branch
-                    (F.col @Double "y" .<= F.lit (3.5 :: Double))
+                    (F.double "y" .<= F.lit (3.5 :: Double))
                     (Leaf "mid")
                     (Leaf "high")
                 ) ::
                 Tree T.Text
         conds =
-            [ (F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double)
-            , (F.col @Double "x" + F.col @Double "y") .> F.lit (4.5 :: Double)
-            , (F.col @Double "x" - F.col @Double "y") .<= F.lit (0.5 :: Double)
-            , (F.col @Double "x" - F.col @Double "y") .> F.lit (0.5 :: Double)
+            [ (F.double "x" + F.double "y") .<= F.lit (4.5 :: Double)
+            , (F.double "x" + F.double "y") .> F.lit (4.5 :: Double)
+            , (F.double "x" - F.double "y") .<= F.lit (0.5 :: Double)
+            , (F.double "x" - F.double "y") .> F.lit (0.5 :: Double)
             ]
         cfg = defaultTreeConfig{taoIterations = 5, expressionPairs = 4, minLeafSize = 1}
         result = taoOptimize @T.Text cfg "label" conds df indices initTree
@@ -413,17 +413,17 @@
 obliqueAxisAlignedFixture =
     let labelExpr =
             F.ifThenElse
-                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))
+                ((F.double "x" + F.double "y") .<= F.lit (4.5 :: Double))
                 (F.lit ("pos" :: T.Text))
                 (F.lit ("neg" :: T.Text))
         df = D.derive @T.Text "label" labelExpr gridBaseDF
         indices = V.enumFromN 0 16
         axisConds =
-            [F.col @Double "x" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]
-                ++ [F.col @Double "y" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]
+            [F.double "x" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]
+                ++ [F.double "y" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]
         initTree =
             Branch
-                (F.col @Double "x" .<= F.lit (2.5 :: Double))
+                (F.double "x" .<= F.lit (2.5 :: Double))
                 (Leaf "pos")
                 (Leaf "neg") ::
                 Tree T.Text
@@ -537,7 +537,7 @@
                         (V.fromList [Nothing, Just (5.0 :: Double)] :: V.Vector (Maybe Double))
                     )
                 ]
-        cond = F.fromMaybe False (F.col @(Maybe Double) "x" .<= F.lit (6.0 :: Double))
+        cond = F.fromMaybe False (F.maybeDouble "x" .<= F.lit (6.0 :: Double))
         (lft, rgt) = partitionIndices cond df (V.fromList [0, 1])
     assertBool "null row (idx 0) routes to right (false) partition" (0 `V.elem` rgt)
     assertBool "Just 5.0 <= 6.0 routes to left (true) partition" (1 `V.elem` lft)
@@ -597,7 +597,7 @@
         [c] ->
             assertBool
                 "cond is isNothing x"
-                (eqExpr c (F.isNothing (F.col @(Maybe Double) "x")))
+                (eqExpr c (F.isNothing (F.maybeDouble "x")))
         _ -> assertFailure "expected exactly one condition"
     assertBool
         "no missingness conds for a non-nullable DataFrame"
@@ -609,7 +609,7 @@
         assertBool
             "generateNumericConds contains isNothing x"
             ( any
-                (`eqExpr` F.isNothing (F.col @(Maybe Double) "x"))
+                (`eqExpr` F.isNothing (F.maybeDouble "x"))
                 (generateNumericConds defaultTreeConfig (D.exclude ["label"] nullsMixedDF))
             )
 
@@ -620,7 +620,7 @@
                 defaultTreeConfig
                 (D.exclude ["label"] nullsMixedDF)
                 nullsMixedDF
-        isMissingCV cv = eqExpr (cvExpr cv) (F.isNothing (F.col @(Maybe Double) "x"))
+        isMissingCV cv = eqExpr (cvExpr cv) (F.isNothing (F.maybeDouble "x"))
         expected = VU.fromList [False, True, False, False, True, False]
     case filter isMissingCV cvs of
         (cv : _) -> assertEqual "isNothing vector marks null slots" expected (cvVec cv)
@@ -640,7 +640,7 @@
                     )
                 ]
         conds = generateNumericConds defaultTreeConfig{percentiles = [50]} df
-        leqAt t = F.fromMaybe False (F.col @(Maybe Double) "x" .<= F.lit (t :: Double))
+        leqAt t = F.fromMaybe False (F.maybeDouble "x" .<= F.lit (t :: Double))
     assertBool
         "median threshold from observed values (30)"
         (any (`eqExpr` leqAt 30) conds)
@@ -667,7 +667,7 @@
     let df =
             D.fromColumns
                 [("s", packedFromTexts (Just [1, 3]) ["yes", "", "no", ""])]
-        isMissingS = F.isNothing (F.col @(Maybe T.Text) "s")
+        isMissingS = F.isNothing (F.maybeText "s")
         conds = missingnessConditions df
     assertEqual "one missingness cond for nullable PackedText" 1 (length conds)
     case conds of
@@ -834,10 +834,10 @@
 taoRecoversNestedObliqueWithoutHint = TestCase $ do
     let labelExpr =
             F.ifThenElse
-                ((F.col @Double "x" + F.col @Double "y") .<= F.lit (4.5 :: Double))
+                ((F.double "x" + F.double "y") .<= F.lit (4.5 :: Double))
                 (F.lit ("low" :: T.Text))
                 ( F.ifThenElse
-                    ((F.col @Double "x" - F.col @Double "y") .<= F.lit (0.5 :: Double))
+                    ((F.double "x" - F.double "y") .<= F.lit (0.5 :: Double))
                     (F.lit "mid")
                     (F.lit "high")
                 )
@@ -845,17 +845,17 @@
         indices = V.enumFromN 0 16
         initTree =
             Branch
-                (F.col @Double "x" .<= F.lit (1.5 :: Double))
+                (F.double "x" .<= F.lit (1.5 :: Double))
                 (Leaf "low")
                 ( Branch
-                    (F.col @Double "y" .<= F.lit (3.5 :: Double))
+                    (F.double "y" .<= F.lit (3.5 :: Double))
                     (Leaf "mid")
                     (Leaf "high")
                 ) ::
                 Tree T.Text
         axisOnlyConds =
-            [F.col @Double "x" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]
-                ++ [F.col @Double "y" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]
+            [F.double "x" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]
+                ++ [F.double "y" .<= F.lit (t :: Double) | t <- [1.5, 2.5, 3.5]]
         cfg =
             defaultTreeConfig
                 { taoIterations = 20
@@ -1161,90 +1161,90 @@
 
 threshAndLeq :: Test
 threshAndLeq = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .<=. F.lit (3.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .<=. F.lit (1.0 :: Double))
+    let a = materializeOrFail (F.double "x" .<=. F.lit (3.0 :: Double))
+        b = materializeOrFail (F.double "x" .<=. F.lit (1.0 :: Double))
         r = combineAndVec a b
     assertEqExpr
         "AND of x≤3 and x≤1 collapses to x≤1"
-        (F.col @Double "x" .<=. F.lit (1.0 :: Double))
+        (F.double "x" .<=. F.lit (1.0 :: Double))
         (cvExpr r)
 
 threshOrLeq :: Test
 threshOrLeq = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .<=. F.lit (3.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .<=. F.lit (1.0 :: Double))
+    let a = materializeOrFail (F.double "x" .<=. F.lit (3.0 :: Double))
+        b = materializeOrFail (F.double "x" .<=. F.lit (1.0 :: Double))
         r = combineOrVec a b
     assertEqExpr
         "OR of x≤3 and x≤1 collapses to x≤3"
-        (F.col @Double "x" .<=. F.lit (3.0 :: Double))
+        (F.double "x" .<=. F.lit (3.0 :: Double))
         (cvExpr r)
 
 threshAndLt :: Test
 threshAndLt = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .<. F.lit (3.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .<. F.lit (1.0 :: Double))
+    let a = materializeOrFail (F.double "x" .<. F.lit (3.0 :: Double))
+        b = materializeOrFail (F.double "x" .<. F.lit (1.0 :: Double))
         r = combineAndVec a b
     assertEqExpr
         "AND of x<3 and x<1 collapses to x<1"
-        (F.col @Double "x" .<. F.lit (1.0 :: Double))
+        (F.double "x" .<. F.lit (1.0 :: Double))
         (cvExpr r)
 
 threshOrLt :: Test
 threshOrLt = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .<. F.lit (3.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .<. F.lit (1.0 :: Double))
+    let a = materializeOrFail (F.double "x" .<. F.lit (3.0 :: Double))
+        b = materializeOrFail (F.double "x" .<. F.lit (1.0 :: Double))
         r = combineOrVec a b
     assertEqExpr
         "OR of x<3 and x<1 collapses to x<3"
-        (F.col @Double "x" .<. F.lit (3.0 :: Double))
+        (F.double "x" .<. F.lit (3.0 :: Double))
         (cvExpr r)
 
 threshAndGeq :: Test
 threshAndGeq = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .>=. F.lit (1.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .>=. F.lit (3.0 :: Double))
+    let a = materializeOrFail (F.double "x" .>=. F.lit (1.0 :: Double))
+        b = materializeOrFail (F.double "x" .>=. F.lit (3.0 :: Double))
         r = combineAndVec a b
     assertEqExpr
         "AND of x≥1 and x≥3 collapses to x≥3"
-        (F.col @Double "x" .>=. F.lit (3.0 :: Double))
+        (F.double "x" .>=. F.lit (3.0 :: Double))
         (cvExpr r)
 
 threshOrGeq :: Test
 threshOrGeq = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .>=. F.lit (1.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .>=. F.lit (3.0 :: Double))
+    let a = materializeOrFail (F.double "x" .>=. F.lit (1.0 :: Double))
+        b = materializeOrFail (F.double "x" .>=. F.lit (3.0 :: Double))
         r = combineOrVec a b
     assertEqExpr
         "OR of x≥1 and x≥3 collapses to x≥1"
-        (F.col @Double "x" .>=. F.lit (1.0 :: Double))
+        (F.double "x" .>=. F.lit (1.0 :: Double))
         (cvExpr r)
 
 threshAndGt :: Test
 threshAndGt = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .>. F.lit (3.0 :: Double))
+    let a = materializeOrFail (F.double "x" .>. F.lit (1.0 :: Double))
+        b = materializeOrFail (F.double "x" .>. F.lit (3.0 :: Double))
         r = combineAndVec a b
     assertEqExpr
         "AND of x>1 and x>3 collapses to x>3"
-        (F.col @Double "x" .>. F.lit (3.0 :: Double))
+        (F.double "x" .>. F.lit (3.0 :: Double))
         (cvExpr r)
 
 threshOrGt :: Test
 threshOrGt = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .>. F.lit (3.0 :: Double))
+    let a = materializeOrFail (F.double "x" .>. F.lit (1.0 :: Double))
+        b = materializeOrFail (F.double "x" .>. F.lit (3.0 :: Double))
         r = combineOrVec a b
     assertEqExpr
         "OR of x>1 and x>3 collapses to x>1"
-        (F.col @Double "x" .>. F.lit (1.0 :: Double))
+        (F.double "x" .>. F.lit (1.0 :: Double))
         (cvExpr r)
 
 -- Six negative cases: rewrite must NOT fire.
 
 threshNegMixedDirection :: Test
 threshNegMixedDirection = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .<. F.lit (3.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .>=. F.lit (1.0 :: Double))
+    let a = materializeOrFail (F.double "x" .<. F.lit (3.0 :: Double))
+        b = materializeOrFail (F.double "x" .>=. F.lit (1.0 :: Double))
         r = combineAndVec a b
     assertEqExpr
         "mixed-direction AND keeps generic F.and form"
@@ -1253,8 +1253,8 @@
 
 threshNegCrossColumn :: Test
 threshNegCrossColumn = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))
-        b = materializeOrFail (F.col @Double "y" .>. F.lit (3.0 :: Double))
+    let a = materializeOrFail (F.double "x" .>. F.lit (1.0 :: Double))
+        b = materializeOrFail (F.double "y" .>. F.lit (3.0 :: Double))
         r = combineAndVec a b
     assertEqExpr
         "cross-column AND keeps generic F.and form"
@@ -1263,8 +1263,8 @@
 
 threshNegMixedOpFamily :: Test
 threshNegMixedOpFamily = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .<. F.lit (4.0 :: Double))
+    let a = materializeOrFail (F.double "x" .>. F.lit (1.0 :: Double))
+        b = materializeOrFail (F.double "x" .<. F.lit (4.0 :: Double))
         r = combineAndVec a b
     assertEqExpr
         "different-op-family AND keeps generic F.and form"
@@ -1273,8 +1273,8 @@
 
 threshNegEqualityOp :: Test
 threshNegEqualityOp = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .==. F.lit (3.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .==. F.lit (1.0 :: Double))
+    let a = materializeOrFail (F.double "x" .==. F.lit (3.0 :: Double))
+        b = materializeOrFail (F.double "x" .==. F.lit (1.0 :: Double))
         r = combineOrVec a b
     assertEqExpr
         "equality OR keeps generic F.or form"
@@ -1283,8 +1283,8 @@
 
 threshNegLitOnLeft :: Test
 threshNegLitOnLeft = TestCase $ do
-    let a = materializeOrFail (F.lit (1.0 :: Double) .<. F.col @Double "x")
-        b = materializeOrFail (F.lit (3.0 :: Double) .<. F.col @Double "x")
+    let a = materializeOrFail (F.lit (1.0 :: Double) .<. F.double "x")
+        b = materializeOrFail (F.lit (3.0 :: Double) .<. F.double "x")
         r = combineAndVec a b
     assertEqExpr
         "Lit-on-left AND keeps generic F.and form"
@@ -1293,8 +1293,8 @@
 
 threshNegNonLiteralRhs :: Test
 threshNegNonLiteralRhs = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .>. F.col @Double "y")
-        b = materializeOrFail (F.col @Double "x" .>. F.lit (3.0 :: Double))
+    let a = materializeOrFail (F.double "x" .>. F.double "y")
+        b = materializeOrFail (F.double "x" .>. F.lit (3.0 :: Double))
         r = combineAndVec a b
     assertEqExpr
         "non-literal RHS AND keeps generic F.and form"
@@ -1304,8 +1304,8 @@
 -- The consolidated cvVec equals the elementwise AND/OR of the inputs.
 threshSemanticPreservation :: Test
 threshSemanticPreservation = TestCase $ do
-    let a = materializeOrFail (F.col @Double "x" .>. F.lit (1.0 :: Double))
-        b = materializeOrFail (F.col @Double "x" .>. F.lit (3.0 :: Double))
+    let a = materializeOrFail (F.double "x" .>. F.lit (1.0 :: Double))
+        b = materializeOrFail (F.double "x" .>. F.lit (3.0 :: Double))
         rAnd = combineAndVec a b
         rOr = combineOrVec a b
         expectedAnd = VU.zipWith (&&) (cvVec a) (cvVec b)
diff --git a/tests-internal/Learn/EdgeCases.hs b/tests-internal/Learn/EdgeCases.hs
--- a/tests-internal/Learn/EdgeCases.hs
+++ b/tests-internal/Learn/EdgeCases.hs
@@ -175,7 +175,7 @@
                 [ ("x", DI.fromList ([-3, -2, -1, -0.5, 0.5, 1, 2, 3] :: [Double]))
                 , ("label", DI.fromList ([0, 0, 0, 0, 1, 1, 1, 1] :: [Int]))
                 ]
-        m = fit defaultLogisticConfig (F.col @Int "label") trainDf
+        m = fit defaultLogisticConfig (F.int "label") trainDf
         probs = logisticProbExprs m
         extremeDf =
             D.fromColumns
@@ -196,7 +196,7 @@
                 [ ("x", DI.fromList ([2] :: [Double]))
                 , ("y", DI.fromList ([7] :: [Double]))
                 ]
-        m = fit defaultLinearConfig (F.col @Double "y") df
+        m = fit defaultLinearConfig (F.double "y") df
         preds = interpD df (predict m)
     assertBool "single OLS prediction finite" (all finite preds)
     assertBool
@@ -211,7 +211,7 @@
                 [ ("x", DI.fromList ([-2, -1, 0, 1, 2] :: [Double]))
                 , ("label", DI.fromList ([1, 1, 1, 1, 1] :: [Int]))
                 ]
-        m = fit defaultLogisticConfig (F.col @Int "label") df
+        m = fit defaultLogisticConfig (F.int "label") df
         preds = interpI df (predict m)
     assertEqual
         "single-class logistic predicts that class everywhere"
@@ -227,7 +227,7 @@
                 , ("const", DI.fromList (replicate 8 (5.0 :: Double)))
                 , ("label", DI.fromList ([0, 0, 0, 0, 1, 1, 1, 1] :: [Int]))
                 ]
-        m = fit defaultLogisticConfig (F.col @Int "label") df
+        m = fit defaultLogisticConfig (F.int "label") df
         preds = interpI df (predict m)
     assertEqual
         "logistic ignores constant column and separates"
@@ -244,7 +244,7 @@
                 , ("const", DI.fromList (replicate 6 (5.0 :: Double)))
                 , ("y", DI.fromList [3 * x + 4 | x <- xs])
                 ]
-        m = fit defaultLinearConfig (F.col @Double "y") df
+        m = fit defaultLinearConfig (F.double "y") df
         coefs = VU.toList (regCoef m)
         names = V.toList (regFeatureNames m)
         byName = zip names coefs
@@ -266,7 +266,7 @@
                 , ("b", DI.fromList ([0, 5, 10] :: [Double]))
                 ]
         cfg = defaultKMeansConfig{kmK = 10, kmNInit = 3, kmSeed = 1}
-        m = fit cfg [F.col @Double "a", F.col @Double "b"] df
+        m = fit cfg [F.double "a", F.double "b"] df
     assertEqual "k clamped to n=3 centres" 3 (V.length (kmCenters m))
     let labels = VU.toList (kmLabels m)
     assertBool "all labels in [0,2]" (all (\l -> l >= 0 && l < 3) labels)
@@ -284,7 +284,7 @@
                 , ("b", DI.fromList (replicate 6 (4.0 :: Double)))
                 ]
         cfg = defaultKMeansConfig{kmK = 2, kmNInit = 3, kmSeed = 1}
-        m = fit cfg [F.col @Double "a", F.col @Double "b"] df
+        m = fit cfg [F.double "a", F.double "b"] df
     assertBool
         "centres finite on degenerate identical data"
         (all (VU.all finite) (V.toList (kmCenters m)))
@@ -301,7 +301,7 @@
                 , ("b", DI.fromList ([0, 10] :: [Double]))
                 ]
         cfg = defaultGMMConfig{gmmK = 5, gmmSeed = 1}
-        m = fit cfg [F.col @Double "a", F.col @Double "b"] df
+        m = fit cfg [F.double "a", F.double "b"] df
     assertEqual "GMM k clamped to n=2" 2 (VU.length (gmmWeights m))
     assertBool "GMM weights finite" (VU.all finite (gmmWeights m))
     assertBool "GMM weights sum to ~1" (close 1e-6 (VU.sum (gmmWeights m)) 1)
@@ -318,7 +318,7 @@
         m =
             fit
                 (PCAConfig (NComp 2) False)
-                [F.col @Double "x", F.col @Double "const"]
+                [F.double "x", F.double "const"]
                 df
         ratio = VU.toList (pcaExplainedVarianceRatio m)
     assertBool "explained ratios finite" (all finite ratio)
@@ -343,7 +343,7 @@
         m =
             fit
                 (PCAConfig (NComp 1) False)
-                [F.col @Double "x", F.col @Double "y"]
+                [F.double "x", F.double "y"]
                 df
         comp0 = pcaComponents m V.! 0
         nrm = sqrt (VU.sum (VU.map (^ (2 :: Int)) comp0))
diff --git a/tests-internal/Learn/NumericalRigor.hs b/tests-internal/Learn/NumericalRigor.hs
--- a/tests-internal/Learn/NumericalRigor.hs
+++ b/tests-internal/Learn/NumericalRigor.hs
@@ -1,6 +1,5 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
 
 -- | Numerical-rigor suite: gradient checks, reproducibility, RNG properties.
 module Learn.NumericalRigor (tests) where
@@ -206,7 +205,7 @@
             kmInertia $
                 fit
                     defaultKMeansConfig{kmK = 2, kmNInit = 1, kmSeed = s}
-                    [F.col @Double "a", F.col @Double "b"]
+                    [F.double "a", F.double "b"]
                     df
         inertias = map fitSeed [0 .. 19]
         blob1 = take 5 (zip as bs)
@@ -261,7 +260,7 @@
 testKMeansReproducible :: Test
 testKMeansReproducible = TestCase $ do
     let cfg s = defaultKMeansConfig{kmK = 4, kmNInit = 1, kmMaxIter = 1, kmSeed = s}
-        run s = fit (cfg s) [F.col @Double "a", F.col @Double "b"]
+        run s = fit (cfg s) [F.double "a", F.double "b"]
         a = run 1 spreadDF
         b = run 1 spreadDF
     assertEqual "k-means same seed identical model" a b
@@ -274,7 +273,7 @@
 testGMMReproducible :: Test
 testGMMReproducible = TestCase $ do
     let cfg s = defaultGMMConfig{gmmK = 2, gmmMaxIter = 1, gmmSeed = s}
-        run s = fit (cfg s) [F.col @Double "a", F.col @Double "b"] blobsDF
+        run s = fit (cfg s) [F.double "a", F.double "b"] blobsDF
         a = run 1
         b = run 1
     assertEqual "GMM same seed identical model" a b
@@ -292,7 +291,7 @@
                 , ("label", DI.fromList ([0, 0, 0, 0, 1, 1, 1, 1] :: [Int]))
                 ]
         cfg s = defaultRFFConfig{rffD = 40, rffGamma = 0.2, rffSeed = s}
-        run s = fit (cfg s) (F.col @Int "label") clsDF
+        run s = fit (cfg s) (F.int "label") clsDF
         a = run 5
         b = run 5
     assertEqual "RFF same seed: same projection B" (rffB a) (rffB b)
diff --git a/tests-internal/Learn/Symbolic.hs b/tests-internal/Learn/Symbolic.hs
--- a/tests-internal/Learn/Symbolic.hs
+++ b/tests-internal/Learn/Symbolic.hs
@@ -47,10 +47,10 @@
                     , srPopSize = 300
                     , srUnaryOps = []
                     }
-                (F.col @Double "y")
+                (F.double "y")
                 df
         preds = interpD df (srBest m)
-        truth = interpD df (F.col @Double "y")
+        truth = interpD df (F.double "y")
         err = sum (zipWith (\p t -> (p - t) ^ (2 :: Int)) preds truth) / 10
     assertBool "SR recovers x*x+x to low error" (err < 1e-6)
     assertBool "Pareto front non-empty" (not (null (srPareto m)))
@@ -66,14 +66,14 @@
         run =
             fit
                 defaultSRConfig{srSeed = 9, srGenerations = 20}
-                (F.col @Double "y")
+                (F.double "y")
                 df
     assertEqual "SR deterministic best MSE" (srBestMSE run) (srBestMSE (rerun df))
   where
     rerun =
         fit
             defaultSRConfig{srSeed = 9, srGenerations = 20}
-            (F.col @Double "y")
+            (F.double "y")
 
 testSimplifyPreservesEval :: Test
 testSimplifyPreservesEval = TestCase $ do
@@ -100,7 +100,7 @@
         m =
             fit
                 defaultKernelPCAConfig{kpcaNComponents = 2, kpcaNLandmarks = 8, kpcaSeed = 1}
-                [F.col @Double "a", F.col @Double "b"]
+                [F.double "a", F.double "b"]
                 df
         pc1 = case kernelPCAExprs m of
             ((_, e) : _) -> interpD df e
@@ -123,7 +123,7 @@
         m =
             fit
                 defaultRFFConfig{rffD = 80, rffGamma = 0.2, rffSeed = 2}
-                (F.col @Int "label")
+                (F.int "label")
                 df
         preds = interpI df (predict m)
     assertEqual "RFF SVM separates" [0, 0, 0, 0, 1, 1, 1, 1] preds
diff --git a/tests-internal/Properties/Simplify.hs b/tests-internal/Properties/Simplify.hs
--- a/tests-internal/Properties/Simplify.hs
+++ b/tests-internal/Properties/Simplify.hs
@@ -61,18 +61,18 @@
     t <- elements thresholds
     oneof
         [ elements
-            [ F.col @Double "x" .< F.lit t
-            , F.col @Double "x" .<= F.lit t
-            , F.col @Double "x" .> F.lit t
-            , F.col @Double "x" .>= F.lit t
-            , F.col @Double "x" .== F.lit t
-            , F.col @Double "x" ./= F.lit t
+            [ F.double "x" .< F.lit t
+            , F.double "x" .<= F.lit t
+            , F.double "x" .> F.lit t
+            , F.double "x" .>= F.lit t
+            , F.double "x" .== F.lit t
+            , F.double "x" ./= F.lit t
             ]
         , elements
-            [ F.toDouble (F.col @Int "n") .< F.lit t
-            , F.toDouble (F.col @Int "n") .<= F.lit t
-            , F.toDouble (F.col @Int "n") .> F.lit t
-            , F.toDouble (F.col @Int "n") .>= F.lit t
+            [ F.toDouble (F.int "n") .< F.lit t
+            , F.toDouble (F.int "n") .<= F.lit t
+            , F.toDouble (F.int "n") .> F.lit t
+            , F.toDouble (F.int "n") .>= F.lit t
             ]
         ]
 
@@ -92,12 +92,12 @@
 genAtomMaybe = do
     t <- elements thresholds
     elements
-        [ F.col @(Maybe Double) "m" .< F.lit t
-        , F.col @(Maybe Double) "m" .<= F.lit t
-        , F.col @(Maybe Double) "m" .> F.lit t
-        , F.col @(Maybe Double) "m" .>= F.lit t
-        , F.col @(Maybe Double) "m" .== F.lit t
-        , F.col @(Maybe Double) "m" ./= F.lit t
+        [ F.maybeDouble "m" .< F.lit t
+        , F.maybeDouble "m" .<= F.lit t
+        , F.maybeDouble "m" .> F.lit t
+        , F.maybeDouble "m" .>= F.lit t
+        , F.maybeDouble "m" .== F.lit t
+        , F.maybeDouble "m" ./= F.lit t
         ]
 
 genMaybeExpr :: Int -> Gen (Expr (Maybe Bool))
diff --git a/tests-internal/TreePruning.hs b/tests-internal/TreePruning.hs
--- a/tests-internal/TreePruning.hs
+++ b/tests-internal/TreePruning.hs
@@ -1,5 +1,4 @@
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeApplications #-}
 
 -- | Specification for the fitted-tree pruning pass (pruneDead).
 module TreePruning (tests) where
@@ -33,19 +32,19 @@
     [ prunesTo
         "ancestor entails child keeps true subtree"
         ( Branch
-            (F.col @Double "age" .> F.lit (50 :: Double))
-            (Branch (F.col @Double "age" .> F.lit (30 :: Double)) (Leaf "a") (Leaf "b"))
+            (F.double "age" .> F.lit (50 :: Double))
+            (Branch (F.double "age" .> F.lit (30 :: Double)) (Leaf "a") (Leaf "b"))
             (Leaf "c")
         )
-        (Branch (F.col @Double "age" .> F.lit (50 :: Double)) (Leaf "a") (Leaf "c"))
+        (Branch (F.double "age" .> F.lit (50 :: Double)) (Leaf "a") (Leaf "c"))
     , prunesTo
         "ancestor refutes child keeps false subtree"
         ( Branch
-            (F.col @Double "age" .> F.lit (50 :: Double))
-            (Branch (F.col @Double "age" .< F.lit (40 :: Double)) (Leaf "a") (Leaf "b"))
+            (F.double "age" .> F.lit (50 :: Double))
+            (Branch (F.double "age" .< F.lit (40 :: Double)) (Leaf "a") (Leaf "b"))
             (Leaf "c")
         )
-        (Branch (F.col @Double "age" .> F.lit (50 :: Double)) (Leaf "b") (Leaf "c"))
+        (Branch (F.double "age" .> F.lit (50 :: Double)) (Leaf "b") (Leaf "c"))
     ]
 
 falseEdgeGate :: [Test]
@@ -53,16 +52,16 @@
     [ prunesTo
         "integral false edge entails child"
         ( Branch
-            (F.toDouble (F.col @Int "ai") .> F.lit (50 :: Double))
+            (F.toDouble (F.int "ai") .> F.lit (50 :: Double))
             (Leaf "c")
             ( Branch
-                (F.toDouble (F.col @Int "ai") .< F.lit (60 :: Double))
+                (F.toDouble (F.int "ai") .< F.lit (60 :: Double))
                 (Leaf "a")
                 (Leaf "b")
             )
         )
         ( Branch
-            (F.toDouble (F.col @Int "ai") .> F.lit (50 :: Double))
+            (F.toDouble (F.int "ai") .> F.lit (50 :: Double))
             (Leaf "c")
             (Leaf "a")
         )
@@ -72,13 +71,13 @@
 sameBranchCollapse =
     [ prunesTo
         "equal leaves collapse the branch"
-        (Branch (F.col @Double "age" .> F.lit (50 :: Double)) (Leaf "a") (Leaf "a"))
+        (Branch (F.double "age" .> F.lit (50 :: Double)) (Leaf "a") (Leaf "a"))
         (Leaf "a")
     , prunesTo
         "collapse cascades upward"
         ( Branch
-            (F.col @Double "age" .> F.lit (50 :: Double))
-            (Branch (F.col @Double "hours" .> F.lit (40 :: Double)) (Leaf "a") (Leaf "a"))
+            (F.double "age" .> F.lit (50 :: Double))
+            (Branch (F.double "hours" .> F.lit (40 :: Double)) (Leaf "a") (Leaf "a"))
             (Leaf "a")
         )
         (Leaf "a")
@@ -89,22 +88,22 @@
     [ preserved
         "child not tight enough is kept"
         ( Branch
-            (F.col @Double "age" .> F.lit (50 :: Double))
-            (Branch (F.col @Double "age" .> F.lit (60 :: Double)) (Leaf "a") (Leaf "b"))
+            (F.double "age" .> F.lit (50 :: Double))
+            (Branch (F.double "age" .> F.lit (60 :: Double)) (Leaf "a") (Leaf "b"))
             (Leaf "c")
         )
     , preserved
         "double false edge is kept (NaN)"
         ( Branch
-            (F.col @Double "weight" .> F.lit (50 :: Double))
+            (F.double "weight" .> F.lit (50 :: Double))
             (Leaf "c")
-            (Branch (F.col @Double "weight" .< F.lit (60 :: Double)) (Leaf "a") (Leaf "b"))
+            (Branch (F.double "weight" .< F.lit (60 :: Double)) (Leaf "a") (Leaf "b"))
         )
     , preserved
         "cross-column descendant is kept"
         ( Branch
-            (F.col @Double "age" .> F.lit (50 :: Double))
-            (Branch (F.col @Double "income" .> F.lit (30000 :: Double)) (Leaf "a") (Leaf "b"))
+            (F.double "age" .> F.lit (50 :: Double))
+            (Branch (F.double "income" .> F.lit (30000 :: Double)) (Leaf "a") (Leaf "b"))
             (Leaf "c")
         )
     ]
diff --git a/tests-internal/Worklist.hs b/tests-internal/Worklist.hs
--- a/tests-internal/Worklist.hs
+++ b/tests-internal/Worklist.hs
@@ -1,5 +1,4 @@
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE TypeApplications #-}
 
 -- | Spec for the saturateCandidates worklist against the boolExprsVec oracle.
 module Worklist (tests, props) where
@@ -51,16 +50,16 @@
         (materializeCondVec fixtureDF e)
 
 xGt, xLt, yGt, yLt, zGt, zLt :: Double -> CondVec
-xGt n = mat (F.col @Double "x" .>. F.lit n)
-xLt n = mat (F.col @Double "x" .<. F.lit n)
-yGt n = mat (F.col @Double "y" .>. F.lit n)
-yLt n = mat (F.col @Double "y" .<. F.lit n)
-zGt n = mat (F.col @Double "z" .>. F.lit n)
-zLt n = mat (F.col @Double "z" .<. F.lit n)
+xGt n = mat (F.double "x" .>. F.lit n)
+xLt n = mat (F.double "x" .<. F.lit n)
+yGt n = mat (F.double "y" .>. F.lit n)
+yLt n = mat (F.double "y" .<. F.lit n)
+zGt n = mat (F.double "z" .>. F.lit n)
+zLt n = mat (F.double "z" .<. F.lit n)
 
 -- Same truth vector as 'xGt 2' but eSize 4 vs 3: a non-degenerate collision.
 notLe2 :: CondVec
-notLe2 = mat (F.not (F.col @Double "x" .<=. F.lit 2))
+notLe2 = mat (F.not (F.double "x" .<=. F.lit 2))
 
 litTrue :: CondVec
 litTrue = mat (F.lit True)
@@ -113,9 +112,9 @@
 
 wideBase :: [CondVec]
 wideBase =
-    [ matW (F.col @Double "a" .>. F.lit 3)
-    , matW (F.col @Double "b" .<. F.lit 5)
-    , matW (F.col @Double "c" .>=. F.lit 3)
+    [ matW (F.double "a" .>. F.lit 3)
+    , matW (F.double "b" .<. F.lit 5)
+    , matW (F.double "c" .>=. F.lit 3)
     ]
 
 tests :: [Test]
