diff --git a/c-buildcfg.cc b/c-buildcfg.cc
index 947f95f..c641fcb 100644
--- a/c-buildcfg.cc
+++ b/c-buildcfg.cc
@@ -140,6 +140,8 @@ T_SRDParserConfig BCLInitDefinitions_( ) noexcept
 			<< M_BCL_TOGGLE_( constantPropagation ) )
 		<< ( Rule( ) << "dead-code-elimination" << Enum( "on-off" )
 			<< M_BCL_TOGGLE_( deadCodeElimination ) )
+		<< ( Rule( ) << "function-inlining" << Enum( "on-off" )
+			<< M_BCL_TOGGLE_( inlineFunctions ) )
 	;
 
 	defs.context( "opt-cf" )
diff --git a/c-buildcfg.hh b/c-buildcfg.hh
index a6ca6aa..654df85 100644
--- a/c-buildcfg.hh
+++ b/c-buildcfg.hh
@@ -34,6 +34,10 @@ struct T_BuildConfiguration
 	// Dead code elimination -----------------------------------------------
 
 		bool deadCodeElimination{ false };
+
+	// Function inlining ---------------------------------------------------
+
+		bool inlineFunctions{ false };
 };
 
 // Table of avaiable build configurations
diff --git a/c-opopt.cc b/c-opopt.cc
index 9c1078c..4f514f3 100644
--- a/c-opopt.cc
+++ b/c-opopt.cc
@@ -1823,3 +1823,93 @@ bool opopt::RemoveDeadCode(
 	}
 	return didStuff;
 }
+
+
+/*= FUNCTION INLINING ========================================================*/
+namespace {
+
+uint32_t IFFindFunctionForNode_(
+		T_OptData const&	data ,
+		const uint32_t		cfgNode
+		) noexcept
+{
+	const auto ncf{ data.cfgFunctions.size( ) };
+	for ( auto i = 0u ; i < ncf ; i ++ ) {
+		auto const& block{ data.cfgFunctions[ i ] };
+		if ( block.first <= cfgNode && block.first + block.count > cfgNode ) {
+			return i;
+		}
+	}
+	return T_HashIndex::INVALID_INDEX;
+}
+
+} // namespace <anon>
+/*----------------------------------------------------------------------------*/
+
+bool opopt::InlineFunctions(
+		T_OpsParserOutput& program ,
+		T_OptData& oData ) noexcept
+{
+	oData.buildControlFlowGraph( program );
+	M_LOGSTR_( "... Inlining functions" , 3 );
+
+	// Find functions that can be inlined
+	auto const& cfg{ oData.ctrlFlowGraph };
+	for ( auto it = cfg.begin( ) ; it != cfg.end( ) ; it ++ ) {
+		auto const& node{ **it };
+
+		// We need a single inbound edge, with CALL type
+		if ( node.inbound.size( ) != 1 ) {
+			continue;
+		}
+		if ( node.inbound[ 0 ].type != T_OptData::T_CtrlFlowEdge::CALL ) {
+			continue;
+		}
+
+		// Find the function we're in and the function the call came from
+		const auto calleeId{ IFFindFunctionForNode_(
+				oData , it.pos( ) ) };
+		const auto callerId{ IFFindFunctionForNode_(
+				oData , node.inbound[ 0 ].target ) };
+
+		// Ignore recursive functions or fake calls to *init*/*frame*
+		if ( callerId == calleeId || callerId == T_HashIndex::INVALID_INDEX ) {
+			continue;
+		}
+
+		// Find corresponding call instruction
+		auto const& callNode{ *oData.ctrlFlowGraph[ node.inbound[ 0 ].target ] };
+		assert( callNode.instructions );
+		const uint32_t callInstrIdx{ callNode.instructions->first
+				+ callNode.instructions->count - 1 };
+		auto const* const instr{ oData.instructions[ callInstrIdx ].node };
+		assert( instr && dynamic_cast< T_CallInstrNode const* >( instr ) );
+
+		// Check call arguments
+		auto const& cInstr{ dynamic_cast< T_CallInstrNode const& >( *instr ) };
+		bool ok{ true };
+		for ( auto i = 0u ; i < cInstr.size( ) && ok ; i ++ ) {
+			auto const& arg{ cInstr.argument( i ) };
+			const auto aet{ arg.expression( ).type( ) };
+			ok = ( aet == A_Node::EXPR_CONST || aet == A_Node::EXPR_ID );
+		}
+		if ( !ok ) {
+			continue;
+		}
+
+		// Add to inlineable functions list
+		// TODO
+		oData.logger( [&](){
+			T_StringBuilder sb;
+			sb << "Will inline function "
+				<< program.root.function( calleeId ).name( );
+			return sb;
+		} , 4 );
+	}
+
+	// Order inline-able functions
+	// Compute extra locals required
+	// Merge (bottom to top)
+
+	return false;
+}
diff --git a/c-opopt.hh b/c-opopt.hh
index 126b9f5..3383938 100644
--- a/c-opopt.hh
+++ b/c-opopt.hh
@@ -217,11 +217,18 @@ bool PropagateConstants(
 		T_OptData& optData ) noexcept;
 
 // Attempt to remove blocks of code that will not be executed because of
-// constant conditions.
+// constant conditions, dead stores, etc...
 //
 bool RemoveDeadCode(
 		T_OpsParserOutput& program ,
 		T_OptData& optData ) noexcept;
 
+// Attempt to inline functions that are only used once and which have simple
+// argument values (constants or identifiers).
+//
+bool InlineFunctions(
+		T_OpsParserOutput& program ,
+		T_OptData& optData ) noexcept;
+
 
 } // namespace opopt
diff --git a/m-builder.cc b/m-builder.cc
index 2664a30..0273929 100644
--- a/m-builder.cc
+++ b/m-builder.cc
@@ -249,19 +249,22 @@ int main( int argc , char** argv )
 			od.curves = &p.curves;
 		}
 
-		bool doneStuff;
+		bool didStuff;
 		do {
-			doneStuff = false;
+			didStuff = false;
 			if ( cfg.constantFolding && opopt::FoldConstants( *parsed , od ) ) {
-				doneStuff = true;
+				didStuff = true;
 			}
 			if ( cfg.constantPropagation && opopt::PropagateConstants( *parsed , od ) ) {
-				doneStuff = true;
+				didStuff = true;
 			}
 			if ( cfg.deadCodeElimination && opopt::RemoveDeadCode( *parsed , od ) ) {
-				doneStuff = true;
+				didStuff = true;
 			}
-		} while ( doneStuff );
+			if ( cfg.inlineFunctions && opopt::InlineFunctions( *parsed , od ) ) {
+				didStuff = true;
+			}
+		} while ( didStuff );
 	}
 
 	// Compile
diff --git a/test/build.srd b/test/build.srd
index 221bc78..05488c7 100644
--- a/test/build.srd
+++ b/test/build.srd
@@ -9,6 +9,7 @@
 		)
 		(constant-propagation on)
 		(dead-code-elimination on)
+		(function-inlining on)
 	)
 )
 
diff --git a/test/demo.srd b/test/demo.srd
index 358f820..1ccbad3 100644
--- a/test/demo.srd
+++ b/test/demo.srd
@@ -362,9 +362,9 @@
 
 (fn bloom-downsample (target level)
 	(locals m w h)
-	(set m (inv (pow 2 $level)))
-	(set w (mul $vp-width $m))
-	(set h (mul $vp-height $m))
+	(set m (pow 2 $level))
+	(set w (div $vp-width $m))
+	(set h (div $vp-height $m))
 
 	(use-pipeline pl-bloom-downsample)
 	(use-framebuffer target)