Index: src/GenPoly/Specialize.cc
===================================================================
--- src/GenPoly/Specialize.cc	(revision 907eccb29a08fe51aa5e5c20aa28f841482b4436)
+++ src/GenPoly/Specialize.cc	(revision 4c8621ac0673e269714072f2f0e355df9f6462de)
@@ -32,4 +32,5 @@
 #include "Common/utility.h"
 #include "InitTweak/InitTweak.h"
+#include "Tuples/Tuples.h"
 
 namespace GenPoly {
@@ -211,16 +212,13 @@
 	// [begin, end) are the formal parameters.
 	// args is the list of arguments currently given to the actual function, the last of which needs to be restructured.
-	template< typename Iterator >
-	void fixLastArg( std::list< Expression * > & args, Iterator begin, Iterator end ) {
-		assertf( ! args.empty(), "Somehow args to tuple function are empty" ); // xxx - it's quite possible this will trigger for the nullary case...
-		Expression * last = args.back();
+	template< typename Iterator, typename OutIterator >
+	void fixLastArg( Expression * last, Iterator begin, Iterator end, OutIterator out ) {
 		// safe_dynamic_cast for the assertion
 		safe_dynamic_cast< TupleType * >( last->get_result() ); // xxx - it's quite possible this will trigger for the unary case...
-		args.pop_back(); // replace last argument in the call with
 		unsigned idx = 0;
 		for ( ; begin != end; ++begin ) {
 			DeclarationWithType * formal = *begin;
 			Type * formalType = formal->get_type();
-			matchOneFormal( last, idx, formalType, back_inserter( args ) );
+			matchOneFormal( last, idx, formalType, out );
 		}
 		delete last;
@@ -251,18 +249,26 @@
 
 		FunctionType * actualType = getFunctionType( actual->get_result() );
-		std::list< DeclarationWithType * >::iterator begin = actualType->get_parameters().begin();
-		std::list< DeclarationWithType * >::iterator end = actualType->get_parameters().end();
-
+		std::list< DeclarationWithType * >::iterator actualBegin = actualType->get_parameters().begin();
+		std::list< DeclarationWithType * >::iterator actualEnd = actualType->get_parameters().end();
+		std::list< DeclarationWithType * >::iterator formalBegin = funType->get_parameters().begin();
+		std::list< DeclarationWithType * >::iterator formalEnd = funType->get_parameters().end();
+
+		Expression * last = nullptr;
 		for ( DeclarationWithType* param : thunkFunc->get_functionType()->get_parameters() ) {
 			// walk the parameters to the actual function alongside the parameters to the thunk to find the location where the ttype parameter begins to satisfy parameters in the actual function.
-			assert( begin != end );
-			++begin;
-
-			// std::cerr << "thunk param: " << param << std::endl;
-			// last param will always be a tuple type... expand it into the actual type(?)
 			param->set_name( paramNamer.newName() );
+			assertf( formalBegin != formalEnd, "Reached end of formal parameters before finding ttype parameter" );
+			if ( Tuples::isTtype((*formalBegin)->get_type()) ) {
+				last = new VariableExpr( param );
+				break;
+			}
+			assertf( actualBegin != actualEnd, "reached end of actual function's arguments before finding ttype parameter" );
+			++actualBegin;
+			++formalBegin;
+
 			appExpr->get_args().push_back( new VariableExpr( param ) );
 		} // for
-		fixLastArg( appExpr->get_args(), --begin, end );
+		assert( last );
+		fixLastArg( last, actualBegin, actualEnd, back_inserter( appExpr->get_args() ) );
 		appExpr->set_env( maybeClone( env ) );
 		if ( inferParams ) {
Index: src/ResolvExpr/AlternativeFinder.cc
===================================================================
--- src/ResolvExpr/AlternativeFinder.cc	(revision 907eccb29a08fe51aa5e5c20aa28f841482b4436)
+++ src/ResolvExpr/AlternativeFinder.cc	(revision 4c8621ac0673e269714072f2f0e355df9f6462de)
@@ -359,25 +359,27 @@
 			}
 			*out++ = new TupleExpr( exprs );
+		} else if ( TypeInstType * ttype = Tuples::isTtype( formalType ) ) {
+			// xxx - mixing default arguments with variadic??
+			std::list< Expression * > exprs;
+			for ( ; actualIt != actualEnd; ++actualIt ) {
+				exprs.push_back( actualIt->expr->clone() );
+				cost += actualIt->cost;
+			}
+			Expression * arg = nullptr;
+			if ( exprs.size() == 1 && Tuples::isTtype( exprs.front()->get_result() ) ) {
+				// the case where a ttype value is passed directly is special, e.g. for argument forwarding purposes
+				// xxx - what if passing multiple arguments, last of which is ttype?
+				// xxx - what would happen if unify was changed so that unifying tuple types flattened both before unifying lists? then pass in TupleType(ttype) below.
+				arg = exprs.front();
+			} else {
+				arg = new TupleExpr( exprs );
+			}
+			assert( arg && arg->get_result() );
+			if ( ! unify( ttype, arg->get_result(), resultEnv, resultNeed, resultHave, openVars, indexer ) ) {
+				return false;
+			}
+			*out++ = arg;
+			return true;
 		} else if ( actualIt != actualEnd ) {
-			if ( TypeInstType * ttype = Tuples::isTtype( formalType ) ) {
-				// xxx - mixing default arguments with variadic??
-				if ( ! Tuples::isTtype( actualIt->expr->get_result() ) ) {
-					// xxx - what if passing multiple arguments, last of which is ttype?
-
-					// consume all remaining arguments, variadic style
-					std::list< Expression * > exprs;
-					for ( ; actualIt != actualEnd; ++actualIt ) {
-						exprs.push_back( actualIt->expr->clone() );
-						cost += actualIt->cost;
-					}
-					TupleExpr * arg = new TupleExpr( exprs );
-					assert( arg->get_result() );
-					// unification run for side effects
-					bool unifyResult = unify( ttype, arg->get_result(), resultEnv, resultNeed, resultHave, openVars, indexer );
-					assertf( unifyResult, "Somehow unifying ttype failed..." );
-					*out++ = arg;
-					return true;
-				}
-			}
 			// both actualType and formalType are atomic (non-tuple) types - if they unify
 			// then accept actual as an argument, otherwise return false (fail to instantiate argument)
Index: src/ResolvExpr/Unify.cc
===================================================================
--- src/ResolvExpr/Unify.cc	(revision 907eccb29a08fe51aa5e5c20aa28f841482b4436)
+++ src/ResolvExpr/Unify.cc	(revision 4c8621ac0673e269714072f2f0e355df9f6462de)
@@ -517,6 +517,20 @@
 			} // if
 		} // for
-		if ( list1Begin != list1End || list2Begin != list2End ) {
-			return false;
+		// if ( list1Begin != list1End || list2Begin != list2End ) {
+		// 	return false;
+		if ( list1Begin != list1End ) {
+			// try unifying empty tuple type with ttype
+			Type * t1 = (*list1Begin)->get_type();
+			if ( Tuples::isTtype( t1 ) ) {
+				Type * combinedType = combineTypes( list2Begin, list2End );
+				return unifyExact( t1, combinedType, env, needAssertions, haveAssertions, openVars, WidenMode( false, false ), indexer );
+			} else return false;
+		} else if ( list2Begin != list2End ) {
+			// try unifying empty tuple type with ttype
+			Type * t2 = (*list2Begin)->get_type();
+			if ( Tuples::isTtype( t2 ) ) {
+				Type * combinedType = combineTypes( list1Begin, list1End );
+				return unifyExact( combinedType, t2, env, needAssertions, haveAssertions, openVars, WidenMode( false, false ), indexer );
+			} else return false;
 		} else {
 			return true;
Index: src/Tuples/TupleAssignment.cc
===================================================================
--- src/Tuples/TupleAssignment.cc	(revision 907eccb29a08fe51aa5e5c20aa28f841482b4436)
+++ src/Tuples/TupleAssignment.cc	(revision 4c8621ac0673e269714072f2f0e355df9f6462de)
@@ -42,4 +42,5 @@
 	  private:
 		void match();
+		void handleEmptyTuple( const ResolvExpr::AltList & alts );
 
 		struct Matcher {
@@ -130,4 +131,13 @@
 						}
 						match();
+					} else {
+						// handle empty case specially. It is easy to cause conflicts for tuple assignment when we consider any expression with Tuple type to be a tuple.
+						// Instead, only tuple expressions and expressions with at least 2 results are considered tuples for tuple assignment. This most obviously leaves out the
+						// nullary and unary cases. The unary case is handled nicely by the alternative finder as is. For example, an expression of type [int] will be exploded
+						// into a list of one element (combined with the RHS elements), which will easily allow for intrinsic construction. This seems like a best case scenario anyway,
+						// since intrinsic construction is much simpler from a code-gen perspective than tuple construction is.
+						// This leaves the empty case, which is not easily handled by existing alternative finder logic. Instead, it seems simple enough to hanlde here that if the left
+						// side is an empty tuple, then the right side is allowed to be either an empty tuple or an empty list. Fortunately, these cases are identical when exploded.
+						handleEmptyTuple( *ali );
 					}
 				}
@@ -248,5 +258,5 @@
 		static UniqueName lhsNamer( "__massassign_L" );
 		static UniqueName rhsNamer( "__massassign_R" );
-		assert ( ! lhs.empty() && rhs.size() <= 1);
+		assert( ! lhs.empty() && rhs.size() <= 1 );
 
 		ObjectDecl * rtmp = rhs.size() == 1 ? newObject( rhsNamer, rhs.front().expr ) : nullptr;
@@ -278,4 +288,24 @@
 		}
 	}
+
+	// empty case is okay when right side is also "empty" (empty explosion handles no argument case as well as empty tuple case)
+	void TupleAssignSpotter::handleEmptyTuple( const ResolvExpr::AltList & alts ) {
+		assert( ! alts.empty() );
+		Expression * lhs = alts.front().expr;
+		if ( PointerType * ptrType = dynamic_cast< PointerType * >( lhs->get_result() ) ) {
+			if ( TupleType * tupleType = dynamic_cast< TupleType * >( ptrType->get_base() ) ) {
+				if ( tupleType->size() == 0 ) {
+					ResolvExpr::AltList rhs;
+					explode( std::next(alts.begin(), 1), alts.end(), currentFinder.get_indexer(), back_inserter(rhs) );
+					if ( rhs.empty() ) {
+						// okay, no other case is allowed
+						ResolvExpr::TypeEnvironment compositeEnv;
+						simpleCombineEnvironments( alts.begin(), alts.end(), compositeEnv );
+						currentFinder.get_alternatives().push_front( ResolvExpr::Alternative( new TupleAssignExpr( std::list< Expression * >(), std::list< ObjectDecl * >() ), compositeEnv, ResolvExpr::sumCost( alts ) ) );
+					}
+				}
+			}
+		}
+	}
 } // namespace Tuples
 
Index: src/Tuples/TupleExpansion.cc
===================================================================
--- src/Tuples/TupleExpansion.cc	(revision 907eccb29a08fe51aa5e5c20aa28f841482b4436)
+++ src/Tuples/TupleExpansion.cc	(revision 4c8621ac0673e269714072f2f0e355df9f6462de)
@@ -225,4 +225,8 @@
 				decl->get_parameters().push_back( tyParam );
 			}
+			if ( tupleType->size() == 0 ) {
+				// empty structs are not standard C. Add a dummy field to empty tuples to silence warnings when a compound literal Tuple0 is created.
+				decl->get_members().push_back( new ObjectDecl( "dummy", DeclarationNode::NoStorageClass, LinkageSpec::C, nullptr, new BasicType( Type::Qualifiers(), BasicType::SignedInt ), nullptr ) );
+			}
 			typeMap[mangleName] = decl;
 			addDeclaration( decl );
