public bool Update(Tokenizer tokenizer, IEnumerable <int> affectedLines)
        {
            var expressionTokenizer = new ExpressionTokenizer(tokenizer, null);
            int groupStart          = 0;

            // we can ignore everything before the first modified line
            if (affectedLines.Any())
            {
                var nextUpdatedLine = affectedLines.Min();
                LOG_UPDATE("Updating lines {0}-{1} (searching {2} groups)", nextUpdatedLine, affectedLines.Max(), Groups.Count);

                while (groupStart < Groups.Count && nextUpdatedLine > Groups[groupStart].LastLine)
                {
                    ++groupStart;
                }

                if (groupStart < Groups.Count)
                {
                    LOG_UPDATE("Found line {0} in group {1} (first line of group is {2})", nextUpdatedLine, groupStart, Groups[groupStart].FirstLine);
                    nextUpdatedLine = Math.Min(Groups[groupStart].FirstLine, nextUpdatedLine);
                }

                expressionTokenizer.PushState();
                expressionTokenizer.AdvanceToLine(nextUpdatedLine);

                if (groupStart > 0)
                {
                    // if the first character to be parsed is not a valid identifier character or comment token,
                    // the new content might need to be merged with the previous group.
                    bool needPreviousGroup;
                    if (Char.IsWhiteSpace(expressionTokenizer.NextChar))
                    {
                        expressionTokenizer.PushState();
                        expressionTokenizer.SkipWhitespace();
                        needPreviousGroup = !IsAtValidGroupStart(expressionTokenizer);
                        expressionTokenizer.PopState();
                    }
                    else
                    {
                        needPreviousGroup = !IsAtValidGroupStart(expressionTokenizer);
                    }

                    if (needPreviousGroup)
                    {
                        --groupStart;
                        nextUpdatedLine = Groups[groupStart].FirstLine;

                        LOG_UPDATE("Also processing group {0} (first line of group is {1})", groupStart, nextUpdatedLine);
                        expressionTokenizer.PopState();
                        expressionTokenizer.AdvanceToLine(nextUpdatedLine);
                    }
                }
            }
            else
            {
                LOG_UPDATE("Updating all lines ({0} groups)", Groups.Count);
            }

            LOG_GROUPS(groupStart - 2, groupStart + 2);

            // parse whatever is remaining
            var newGroups = new List <ExpressionGroup>();

            ParseGroups(expressionTokenizer, newGroups);

            // attempt to match the end of the script
            int groupStop    = Groups.Count;
            int newGroupStop = newGroups.Count;

            if (newGroupStop > 0)
            {
                while (groupStop > groupStart)
                {
                    var existingGroup = Groups[--groupStop];
                    var newGroup      = newGroups[--newGroupStop];

                    if (!existingGroup.ExpressionsMatch(newGroup))
                    {
                        ++groupStop;
                        ++newGroupStop;
                        break;
                    }

                    var firstLine = existingGroup.FirstLine;
                    var lastLine  = existingGroup.LastLine;

                    existingGroup.ReplaceExpressions(newGroup, false);
                    Scope.UpdateVariables(existingGroup.Modifies, newGroup);

                    var adjustment = existingGroup.FirstLine - firstLine;
                    if (adjustment != 0)
                    {
                        if (existingGroup.GeneratedAchievements != null)
                        {
                            foreach (var achievement in existingGroup.GeneratedAchievements)
                            {
                                achievement.SourceLine += adjustment;
                            }
                        }
                        if (existingGroup.GeneratedLeaderboards != null)
                        {
                            foreach (var leaderboard in existingGroup.GeneratedLeaderboards)
                            {
                                leaderboard.SourceLine += adjustment;
                            }
                        }

                        foreach (var error in _evaluationErrors)
                        {
                            var innerError = error;
                            while (innerError != null)
                            {
                                if (innerError.Location.Start.Line >= firstLine && innerError.Location.End.Line <= lastLine)
                                {
                                    innerError.AdjustLines(adjustment);
                                }

                                innerError = innerError.InnerError;
                            }
                        }
                    }

                    if (newGroupStop == 0)
                    {
                        if (groupStop == groupStart)
                        {
                            // no change detected
                            return(false);
                        }

                        // groups were removed
                        break;
                    }
                }
            }

            // whatever is remaining will be swapped out.
            // capture any affected variables and remove associated evaluation errors
            var affectedVariables = new HashSet <string>();

            for (int i = groupStart; i < groupStop; ++i)
            {
                var group = Groups[i];
                foreach (var variable in group.Modifies)
                {
                    affectedVariables.Add(variable);
                    Scope.UndefineVariable(variable);
                    Scope.UndefineFunction(variable);
                }

                for (int j = _evaluationErrors.Count - 1; j >= 0; j--)
                {
                    var error = _evaluationErrors[j];
                    if (error.Location.End.Line >= group.FirstLine && error.Location.Start.Line <= group.LastLine)
                    {
                        _evaluationErrors.RemoveAt(j);
                    }
                    else if (error.InnerError != null)
                    {
                        error = error.InnermostError;
                        if (error.Location.End.Line >= group.FirstLine && error.Location.Start.Line <= group.LastLine)
                        {
                            _evaluationErrors.RemoveAt(j);
                        }
                    }
                }
            }

            // also capture any affected variables for groups being swapped in, and determine
            // if they need to be evaluated.
            for (int i = 0; i < newGroupStop; ++i)
            {
                var newGroup = newGroups[i];
                newGroup.UpdateMetadata();
                newGroup.MarkForEvaluation();

                foreach (var variable in newGroup.Modifies)
                {
                    affectedVariables.Add(variable);
                }
            }

            // perform the swap
            if (newGroupStop == 0)
            {
                if (groupStart < Groups.Count)
                {
                    LOG_UPDATE("Removing groups {0}-{1} (lines {2}-{3})",
                               groupStart, groupStop - 1, Groups[groupStart].FirstLine, Groups[groupStop - 1].LastLine);

                    Groups.RemoveRange(groupStart, groupStop - groupStart);
                }
            }
            else if (groupStop == groupStart)
            {
                LOG_UPDATE("Adding {0} groups (lines {1}-{2})",
                           newGroupStop, newGroups[0].FirstLine, newGroups[newGroupStop - 1].LastLine);

                Groups.InsertRange(groupStart, newGroups.Take(newGroupStop));
            }
            else
            {
                LOG_UPDATE("Replacing groups {0}-{1} (lines {2}-{3}) with {4} groups (lines {5}-{6})",
                           groupStart, groupStop - 1, Groups[groupStart].FirstLine, Groups[groupStop - 1].LastLine,
                           newGroupStop, newGroups[0].FirstLine, newGroups[newGroupStop - 1].LastLine);

                Groups.RemoveRange(groupStart, groupStop - groupStart);
                Groups.InsertRange(groupStart, newGroups.Take(newGroupStop));
            }

            LOG_GROUPS(groupStart - 2, groupStart + newGroupStop + 2);

            bool needsEvaluated = false;

            // re-evaluate any groups that are dependent on (or modify) the affected variables
            if (affectedVariables.Count > 0)
            {
                FlagDependencies(affectedVariables);
                needsEvaluated = true;
            }
            else
            {
                for (int i = 0; i < newGroupStop; ++i)
                {
                    if (newGroups[i].NeedsEvaluated)
                    {
                        needsEvaluated = true;
                        break;
                    }
                }
            }

            return(needsEvaluated);
        }