diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,30 @@
 # Changelog for math-programming-glpk
 
-## Unreleased changes
+## [0.4.0] -- 5 July 2020
+
+### Changed
+
+- Updated to the `math-programming-0.4.0` API.
+
+### Fixed
+
+- How variable bounds are set.
+
+  Free variables were being constrained to equal zero; they are now
+  correctly set as free variables.
+
+### Added
+
+- The `writeFormulation` function.
+
+  This replaces the `writeFormulation` class method.
+
+### Removed
+
+- The `writeFormulation` class method.
+
+  This is consistent with the `math-programming-0.4.0` API.
+
+## [0.3.0] -- 18 June 2020
+
+Initial release.
diff --git a/math-programming-glpk.cabal b/math-programming-glpk.cabal
--- a/math-programming-glpk.cabal
+++ b/math-programming-glpk.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.12
 name:               math-programming-glpk
-version:            0.3.0
+version:            0.4.0
 license:            BSD3
 license-file:       LICENSE
 copyright:          2018 Patrick Steele
@@ -35,7 +35,7 @@
         base >=4.7 && <5,
         containers >=0.6.0.1 && <0.7,
         glpk-headers >=0.4.0 && <0.5,
-        math-programming >=0.3.0 && <0.4,
+        math-programming >=0.4.0 && <0.5,
         mtl >=2.2.2 && <2.3,
         text >=1.2.3.1 && <1.3
 
@@ -45,6 +45,7 @@
     hs-source-dirs:   test
     other-modules:
         ApiTests
+        RegressionTests
         Paths_math_programming_glpk
 
     default-language: Haskell2010
@@ -54,9 +55,9 @@
         base >=4.7 && <5,
         containers >=0.6.0.1 && <0.7,
         glpk-headers >=0.4.0 && <0.5,
-        math-programming >=0.3.0 && <0.4,
-        math-programming-glpk,
-        math-programming-tests >=0.3.0 && <0.4,
+        math-programming >=0.4.0 && <0.5,
+        math-programming-glpk -any,
+        math-programming-tests >=0.4.0 && <0.5,
         mtl >=2.2.2 && <2.3,
         tasty >=1.2.3 && <1.3,
         tasty-discover >=4.2.1 && <4.3,
diff --git a/src/Math/Programming/Glpk.hs b/src/Math/Programming/Glpk.hs
--- a/src/Math/Programming/Glpk.hs
+++ b/src/Math/Programming/Glpk.hs
@@ -4,6 +4,7 @@
 module Math.Programming.Glpk
   ( Glpk
   , runGlpk
+  , writeFormulation
   -- ** Controlling GLPK behavior
   -- $settings
   , GlpkEnv
diff --git a/src/Math/Programming/Glpk/Internal.hs b/src/Math/Programming/Glpk/Internal.hs
--- a/src/Math/Programming/Glpk/Internal.hs
+++ b/src/Math/Programming/Glpk/Internal.hs
@@ -79,7 +79,6 @@
   getTimeout = getTimeout'
   setTimeout = setTimeout'
   optimizeLP = optimizeLP'
-  writeFormulation = writeFormulation'
 
 instance IPMonad Glpk where
   optimizeIP = optimizeIP'
@@ -218,6 +217,9 @@
   problem <- askProblem
   variable <- liftIO $ do
     column <- glp_add_cols problem 1
+
+    glp_set_col_bnds problem column glpkFree 0 0
+
     columnRef <- newIORef column
     return $ NamedRef (fromIntegral column) columnRef
   register askVariablesRef variable
@@ -251,7 +253,7 @@
     constraintType = case ordering of
       LT -> glpkLT
       GT -> glpkGT
-      EQ -> glpkBounded
+      EQ -> glpkFixed
 
     constraintRhs :: CDouble
     constraintRhs = realToFrac (negate constant)
@@ -524,8 +526,9 @@
   | status == glpkUnbounded  = Unbounded
   | otherwise                = Error
 
-writeFormulation' :: FilePath -> Glpk ()
-writeFormulation' fileName = do
+-- | Write out the current formulation to a file.
+writeFormulation :: FilePath -> Glpk ()
+writeFormulation fileName = do
   problem <- askProblem
   _ <- liftIO $ withCString fileName (glp_write_lp problem nullPtr)
   return ()
diff --git a/test/RegressionTests.hs b/test/RegressionTests.hs
new file mode 100644
--- /dev/null
+++ b/test/RegressionTests.hs
@@ -0,0 +1,70 @@
+module RegressionTests where
+
+import           Control.Monad.IO.Class
+
+import           Test.Tasty
+import           Test.Tasty.HUnit
+
+import           Math.Programming
+import           Math.Programming.Glpk
+
+test_tree :: TestTree
+test_tree = testGroup "Regression tests"
+            [ testCase "Free variables (LP)" testFreeVariablesLP
+            , testCase "Free variables (IP)" testFreeVariablesIP
+            ]
+
+assertFeasible :: SolutionStatus -> Glpk ()
+assertFeasible result
+  = liftIO $ case result of
+      Error      -> assertFailure "Failed to solve program"
+      Unbounded  -> assertFailure "Unbounded program"
+      Infeasible -> assertFailure "Infeasible program"
+      _          -> pure ()
+
+assertRunGlpk :: Glpk a -> IO ()
+assertRunGlpk program = do
+  eResult <- runGlpk program
+  case eResult of
+    Left err -> assertFailure ("Error solving program: " <> show err)
+    Right _  -> pure ()
+
+testFreeVariablesLP :: IO ()
+testFreeVariablesLP = assertRunGlpk $ do
+  x <- free
+  y <- free
+  z <- free
+
+  _ <- x @==# 0
+  _ <- y @==# 3.1
+  _ <- z @==# -3.1
+
+  optimizeLP >>= assertFeasible
+
+  vx <- getVariableValue x
+  vy <- getVariableValue y
+  vz <- getVariableValue z
+
+  liftIO $ 0 @=? vx
+  liftIO $ 3.1 @=? vy
+  liftIO $ -3.1 @=? vz
+
+testFreeVariablesIP :: IO ()
+testFreeVariablesIP = assertRunGlpk $ do
+  x <- integer
+  y <- integer
+  z <- integer
+
+  _ <- x @==# 0
+  _ <- y @==# 3
+  _ <- z @==# -3
+
+  optimizeIP >>= assertFeasible
+
+  vx <- getVariableValue x
+  vy <- getVariableValue y
+  vz <- getVariableValue z
+
+  liftIO $ 0 @=? vx
+  liftIO $ 3 @=? vy
+  liftIO $ -3 @=? vz
