public void EmitLoad(Line line)
        {
            // Find every variable that is loaded anywhere in the line
            var loadFinder = new FindReadVariables();

            loadFinder.Visit(line);

            // Find every variable that is written to anywhere in the line
            var storeFinder = new FindAssignedVariables();

            storeFinder.Visit(line);
            _mutated.UnionWith(storeFinder.Names);

            // Only cache things which read or written more than once in the line
            var toCache = new HashSet <VariableName>();

            foreach (var(name, count) in loadFinder.Counts)
            {
                if (count > 1)
                {
                    toCache.Add(name);
                }
            }
            foreach (var(name, count) in storeFinder.Counts)
            {
                if (count > 1)
                {
                    toCache.Add(name);
                }
            }

            // Load up everything that will be read later in the line
            // The `IsDirty` flags protect against writing back uninitialised variables at the end
            foreach (var variable in toCache)
            {
                var type  = _types.TypeOf(variable) ?? StackType.YololValue;
                var local = _emitter.DeclareLocal(type.ToType(), $"CacheFor_{variable.Name}", false);

                EmitLoadValue(variable);
                StaticUnbox(type);
                _emitter.StoreLocal(local);
                _cache.Add(variable, new TypedLocal(type, local));

                var dirty = _emitter.DeclareLocal(typeof(bool), $"CacheFor_{variable.Name}_IsDirty");
                _emitter.LoadConstant(false);
                _emitter.StoreLocal(dirty);
                _cacheDirty.Add(variable, dirty);
            }
        }
        /// <summary>
        /// Compile a line of Yolol into the given IL emitter
        /// </summary>
        /// <param name="line"></param>
        /// <param name="emit"></param>
        /// <param name="lineNumber"></param>
        /// <param name="maxLines"></param>
        /// <param name="maxStringLength"></param>
        /// <param name="internalVariableMap"></param>
        /// <param name="externalVariableMap"></param>
        /// <param name="staticTypes"></param>
        /// <param name="changeDetection"></param>
        public static void Compile(
            this Line line,
            Emit <Func <ArraySegment <Value>, ArraySegment <Value>, LineResult> > emit,
            int lineNumber,
            int maxLines,
            int maxStringLength,
            IReadonlyInternalsMap internalVariableMap,
            IReadonlyExternalsMap externalVariableMap,
            IReadOnlyDictionary <VariableName, Type>?staticTypes = null,
            bool changeDetection = false
            )
        {
            using (var emitter = new OptimisingEmitter <Func <ArraySegment <Value>, ArraySegment <Value>, LineResult> >(emit))
            {
                void EmitFallthroughCalc()
                {
                    if (lineNumber == maxLines)
                    {
                        emitter.LoadConstant(1);
                    }
                    else
                    {
                        emitter.LoadConstant(lineNumber + 1);
                    }
                }

                // Special case for totally empty lines
                if (line.Statements.Statements.Count == 0)
                {
                    EmitFallthroughCalc();
                    emitter.LoadConstant(0ul);
                    emitter.NewObject <ChangeSet, ulong>();
                    emitter.NewObject(typeof(LineResult).GetConstructor(new[] { typeof(int), typeof(ChangeSet) }) !);
                    emitter.Return();
                    return;
                }

                // Begin an exception block to catch Yolol runtime errors
                var exBlock = emitter.BeginExceptionBlock();

                // Create a local to store the return address from inside the try/catch block
                var retAddr = emitter.DeclareLocal <int>("ret_addr", initializeReused: false);

                // Create a local for the change bit set
                var changeSet = changeDetection ? emitter.DeclareLocal <ulong>("change_set") : null;

                // Store the default return address to go to
                EmitFallthroughCalc();
                emitter.StoreLocal(retAddr);

                // Create a label which any `goto` statements can use. They drop their destination PC on the stack and then jump to this label
                var gotoLabel = emitter.DefineLabel2("encountered_goto");

                var types = new StaticTypeTracker(staticTypes);

                // Create a memory accessor which manages reading and writing the memory arrays
                using (var accessor = new ArraySegmentMemoryAccessor <Func <ArraySegment <Value>, ArraySegment <Value>, LineResult> >(
                           emitter,
                           1,
                           0,
                           internalVariableMap,
                           externalVariableMap,
                           types,
                           changeSet
                           ))
                {
                    accessor.EmitLoad(line);

                    // Convert the entire line into IL
                    var converter = new ConvertLineVisitor <Func <ArraySegment <Value>, ArraySegment <Value>, LineResult> >(emitter, maxLines, accessor, exBlock, gotoLabel, types, maxStringLength);
                    converter.Visit(line);

                    // When a line finishes (with no gotos in the line) call flow eventually reaches here. Go to the next line.
                    emitter.Leave(exBlock);

                    // Create a block to handle gotos. The destination will already be on the stack, so just return
                    if (gotoLabel.IsUsed)
                    {
                        emitter.MarkLabel(gotoLabel);
                        emitter.StoreLocal(retAddr);
                        emitter.Leave(exBlock);
                    }

                    // Catch all execution exceptions and return the appropriate next line number to fall through to
                    var catchBlock = emitter.BeginCatchBlock <ExecutionException>(exBlock);
#if DEBUG
                    using (var ex = emitter.DeclareLocal(typeof(ExecutionException), initializeReused: false))
                    {
                        emitter.StoreLocal(ex);
                        emitter.WriteLine("execution exception: {0}", ex);
                    }
#else
                    emitter.Pop();
#endif

                    // Close the exception block which was wrapping the entire method
                    emitter.EndCatchBlock(catchBlock);
                    emitter.EndExceptionBlock(exBlock);
                }

                // Load the return address from inside the catch block
                emitter.LoadLocal(retAddr);

                // Create the change set, either from the computer value or the default value (which indicates that everything has changed)
                if (changeSet != null)
                {
                    emitter.LoadLocal(changeSet);
                }
                else
                {
                    emitter.LoadConstant(ulong.MaxValue);
                }
                emitter.NewObject <ChangeSet, ulong>();

                emitter.NewObject(typeof(LineResult).GetConstructor(new[] { typeof(int), typeof(ChangeSet) }) !);
                emitter.Return();

                emitter.Optimise();

#if DEBUG
                Console.WriteLine($"// {line}");
                Console.WriteLine(emitter.ToString());
                Console.WriteLine($"------------------------------");
#endif
            }
        }