diff --git a/Makefile b/Makefile
index d45d2ed..d41e530 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,7 @@ DEMO = \
 	 odbg.cc \
 	 sync.cc \
 	 control.cc \
+	 opast.cc \
 	 \
 	 demo.cc \
 	 \
diff --git a/demo.srd b/demo.srd
index 81e75f5..ba0aa66 100644
--- a/demo.srd
+++ b/demo.srd
@@ -1,4 +1,4 @@
-(fn init ()
+(init
 	# Compute viewport size
 	(if (gt width height)
 		(
@@ -18,7 +18,7 @@
 	(call dof-init)
 )
 
-(fn frame ()
+(frame
 	(profiling "Frame render"
 		{ FIXME: other stages }
 		(call dof-main { FIXME: args here })
diff --git a/opast.cc b/opast.cc
new file mode 100644
index 0000000..5ac8adf
--- /dev/null
+++ b/opast.cc
@@ -0,0 +1,217 @@
+#include "opast.hh"
+
+using namespace ebcl;
+using namespace opast;
+
+
+/*= A_Node ===================================================================*/
+
+A_Node::A_Node( const E_Type type ,
+		A_Node* const parent ) noexcept
+	: type_( type ) , parent_( parent )
+{
+	assert( ( type == ROOT && !parent ) || ( type != ROOT && parent ) );
+}
+
+A_Node::~A_Node( ) { }
+
+T_RootNode& A_Node::root( ) const noexcept
+{
+	A_Node const* node( this );
+	while ( node->parent_ ) {
+		node = node->parent_;
+	}
+	assert( node );
+	assert( node->type_ == ROOT );
+	return *dynamic_cast< T_RootNode* >(
+			const_cast< A_Node* >( node ) );
+}
+
+/*= A_FuncNode ===============================================================*/
+
+A_FuncNode::A_FuncNode(
+		const bool isInit ,
+		T_RootNode* const root ) noexcept
+	: A_Node( isInit ? DECL_INIT : DECL_FRAME , root ) ,
+		name_( isInit ? "*init*" : "*frame*" )
+{ }
+
+A_FuncNode::A_FuncNode(
+		T_String const& name ,
+		T_RootNode* const root ) noexcept
+	: A_Node( DECL_FN , root ) , name_( name )
+{ }
+
+
+/*= T_RootNode ===============================================================*/
+
+T_RootNode::T_RootNode( ) noexcept
+	: A_Node( ROOT , nullptr ) , functions_(
+			[]( T_OwnPtr< A_FuncNode > const& f ) {
+				return f->name( );
+			} )
+{ }
+
+
+T_RootNode::T_AddFunctionResult T_RootNode::addFunction(
+		T_OwnPtr< A_FuncNode >& function ) noexcept
+{
+	T_String const& fn( function->name( ) );
+	auto const* const pf( functions_.get( fn ) );
+	if ( !pf ) {
+		auto* const rv( function.get( ) );
+		functions_.add( std::move( function ) );
+		return *rv;
+	}
+
+	T_String dfn;
+	uint32_t dupCtr( 0 );
+	do {
+		T_StringBuilder fnsb;
+		fnsb << fn << " dup " << dupCtr ++;
+		dfn = std::move( fnsb );
+	} while ( functions_.contains( dfn ) );
+
+	T_OwnPtr< A_FuncNode > df( NewOwned< T_FuncNode >( dfn , *this ) );
+	auto* const rv( df.get( ) );
+	functions_.add( std::move( df ) );
+	return T_AddFunctionResult{ *rv , (*pf)->location( ) };
+}
+
+
+/*= T_SpecialFuncNode ========================================================*/
+
+T_SpecialFuncNode::T_SpecialFuncNode(
+		bool isInit ,
+		T_RootNode& parent ) noexcept
+	: A_FuncNode( isInit , &parent )
+{ }
+
+
+/*= T_FuncNode ===============================================================*/
+
+T_FuncNode::T_FuncNode(
+		T_String const& name ,
+		T_RootNode& parent ) noexcept
+	: A_FuncNode( name , &parent )
+{ }
+
+T_Optional< T_SRDLocation > T_FuncNode::addArgument(
+		T_SRDToken const& token ) noexcept
+{
+	assert( token.type( ) == E_SRDTokenType::WORD );
+	assert( token.hasLocation( ) );
+
+	const auto pnp( argNames_.indexOf( token.stringValue( ) ) );
+	if ( pnp != -1 ) {
+		return argLocations_[ pnp ];
+	}
+	argNames_.add( token.stringValue( ) );
+	argLocations_.add( token.location( ) );
+	return {};
+}
+
+
+/*= T_Parser =================================================================*/
+
+namespace {
+
+struct T_ParserImpl_
+{
+	T_OwnPtr< T_RootNode >& root;
+	T_Array< T_SRDError >& errors;
+
+	T_ParserImpl_( T_Array< T_SRDError >* errors ,
+			T_OwnPtr< T_RootNode >* root ) noexcept;
+
+	void parseFunction( T_SRDList const& funcList ) noexcept;
+};
+
+T_ParserImpl_::T_ParserImpl_(
+		T_Array< T_SRDError >* const errors ,
+		T_OwnPtr< T_RootNode >* const root ) noexcept
+	: root( *root ) , errors( *errors )
+{ }
+
+void T_ParserImpl_::parseFunction(
+		T_SRDList const& funcList ) noexcept
+{
+	assert( funcList.size( ) != 0 );
+
+	auto const& fw( funcList[ 0 ] );
+	if ( fw.type( ) != E_SRDTokenType::WORD
+			|| ( fw.stringValue( ) != "init" && fw.stringValue( ) != "frame"
+				&& fw.stringValue( ) != "fn" ) ) {
+		errors.addNew( "init, frame or fn expected" , fw.location( ) );
+		return;
+	}
+
+	T_String const& ftw( fw.stringValue( ) );
+	T_OwnPtr< A_FuncNode > fn;
+	if ( ftw == "fn" ) {
+		if ( funcList.size( ) < 3 ) {
+			errors.addNew( "function name and arguments expected" ,
+					fw.location( ) );
+			return;
+		}
+		if ( funcList[ 1 ].type( ) != E_SRDTokenType::WORD ) {
+			errors.addNew( "function name expected" , funcList[ 1 ].location( ) );
+			return;
+		}
+
+		fn = NewOwned< T_FuncNode >( funcList[ 1 ].stringValue( ) , *root );
+		// TODO parseFunctionArguments( funcList[ 2 ] );
+	} else {
+		fn = NewOwned< T_SpecialFuncNode >( ftw == "init" , *root );
+	}
+	fn->location( ) = fw.location( );
+
+	const auto af( root->addFunction( fn ) );
+	if ( af.dupLocation.present( ) ) {
+		T_StringBuilder esb( "duplicate " );
+		switch ( fn->type( ) ) {
+			case A_Node::DECL_FN:
+				esb << "function '" << fn->name( ) << "'";
+				break;
+			case A_Node::DECL_INIT:
+				esb << "initialisation function";
+				break;
+			case A_Node::DECL_FRAME:
+				esb << "frame function";
+				break;
+			default: std::abort( );
+		}
+		esb << "; previous declaration: " << *af.dupLocation.target( );
+		errors.addNew( std::move( esb ) , fw.location( ) );
+	}
+
+	// TODO parseInstructions( fn->instructions , funcList ,
+	// 			ftw == "fn" ? 3 : 1 );
+}
+
+} // namespace
+
+/*----------------------------------------------------------------------------*/
+
+T_Parser::T_Parser( ) noexcept
+	: A_PrivateImplementation( new T_ParserImpl_( &errors_ , &rootNode_ ) ) ,
+		errors_( 64 ) , rootNode_{}
+{}
+
+bool T_Parser::parse(
+		T_SRDList const& input ) noexcept
+{
+	errors_.clear( );
+	rootNode_ = NewOwned< T_RootNode >( );
+
+	for ( auto const& t : input ) {
+		if ( t.type( ) == E_SRDTokenType::LIST && t.list( ).size( ) > 0 ) {
+			p< T_ParserImpl_ >( ).parseFunction( t.list( ) );
+		} else {
+			errors_.addNew( "Function or special block expected" ,
+					t.location( ) );
+		}
+	}
+
+	return errors_.empty( );
+}
diff --git a/opast.hh b/opast.hh
new file mode 100644
index 0000000..f0ac73f
--- /dev/null
+++ b/opast.hh
@@ -0,0 +1,170 @@
+#pragma once
+//#ifndef REAL_BUILD
+# include "externals.hh"
+//#endif
+
+#include <ebcl/SRDData.hh>
+
+
+namespace opast {
+
+using namespace ebcl;
+
+class T_RootNode;
+
+class A_Node
+{
+    public:
+	enum E_Type {
+		ROOT ,
+		//
+		DECL_INIT ,
+		DECL_FRAME ,
+		DECL_FN ,
+		//
+		OP_SET ,
+		//
+		EXPR_ADD ,
+		EXPR_MUL ,
+		EXPR_SUB ,
+		EXPR_DIV ,
+		EXPR_VAR ,
+		EXPR_CONST ,
+	};
+
+    private:
+	const E_Type type_;
+	A_Node* const parent_;
+	T_SRDLocation location_;
+
+    protected:
+	explicit A_Node( const E_Type type ,
+			A_Node* const parent ) noexcept;
+
+    public:
+	virtual ~A_Node( ) = 0;
+
+	E_Type type( ) const noexcept
+		{ return type_; }
+
+	T_SRDLocation& location( ) noexcept
+		{ return location_; }
+	T_SRDLocation const& location( ) const noexcept
+		{ return location_; }
+
+	A_Node& parent( ) const noexcept
+		{ assert( parent_ ); return *parent_; }
+
+	T_RootNode& root( ) const noexcept;
+};
+
+/*----------------------------------------------------------------------------*/
+
+// Function-like nodes
+class A_FuncNode : public A_Node
+{
+    private:
+	T_String name_;
+
+    protected:
+	// For init or frame entry points.
+	// isInit = true => init entry point
+	// isInit = false => frame entry point
+	explicit A_FuncNode( bool isInit ,
+			T_RootNode* const parent ) noexcept;
+
+	// For normal functions
+	explicit A_FuncNode( T_String const& name ,
+			T_RootNode* const parent ) noexcept;
+
+    public:
+	T_String const& name( ) const noexcept
+		{ return name_; }
+};
+
+// Root node, keeps track of the whole tree and related data (function table,
+// assets...)
+class T_RootNode : public A_Node
+{
+    private:
+	T_ObjectTable< T_String , T_OwnPtr< A_FuncNode > > functions_;
+
+    public:
+	T_RootNode( ) noexcept;
+
+	// Return type for addFunction. We'll always return a reference to a
+	// function node (which may or may not be the same as the initial one,
+	// if there were duplicates), and we'll return the location of the
+	// initial function in the case of a duplicate.
+	struct T_AddFunctionResult
+	{
+		A_FuncNode& function;
+		T_Optional< T_SRDLocation > dupLocation;
+
+		T_AddFunctionResult( A_FuncNode& function ) noexcept
+			: function{ function } , dupLocation{}
+		{}
+
+		T_AddFunctionResult( A_FuncNode& function ,
+				T_SRDLocation const& dupLocation ) noexcept
+			: function{ function } , dupLocation{ dupLocation }
+		{}
+	};
+
+	// Attempts to add a function. If the function is already present in
+	// the table, the result will be set up with the previous declaration's
+	// location, and the specified function will not be modified (a duplicate
+	// entry will be added to the table instead).
+	T_AddFunctionResult addFunction(
+			T_OwnPtr< A_FuncNode >& function ) noexcept;
+};
+
+/*----------------------------------------------------------------------------*/
+
+// Init & frame functions
+class T_SpecialFuncNode : public A_FuncNode
+{
+    public:
+	T_SpecialFuncNode(
+			bool isInit ,
+			T_RootNode& parent ) noexcept;
+};
+
+// Normal functions
+class T_FuncNode : public A_FuncNode
+{
+    private:
+	T_AutoArray< T_String , 8 > argNames_;
+	T_AutoArray< T_SRDLocation , 8 > argLocations_;
+
+    public:
+	T_FuncNode( T_String const& name ,
+			T_RootNode& parent ) noexcept;
+
+	// Add an argument. If the argument is a duplicate, return the location
+	// of the initial argument.
+	T_Optional< T_SRDLocation > addArgument(
+			T_SRDToken const& token ) noexcept;
+};
+
+/*----------------------------------------------------------------------------*/
+
+class T_Parser : public A_PrivateImplementation
+{
+    private:
+	T_Array< T_SRDError > errors_;
+	T_OwnPtr< T_RootNode > rootNode_;
+
+    public:
+	T_Parser( ) noexcept;
+
+	bool parse( T_SRDList const& input ) noexcept;
+
+	T_Array< T_SRDError > const& errors( ) const noexcept
+		{ return errors_; }
+	T_OwnPtr< T_RootNode > result( ) noexcept
+		{ return std::move( rootNode_ ); }
+};
+
+
+} // namespace opast