diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 6485c16..b1a2955 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,1052 +1,1054 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'arc liberate' to rebuild it.
  *
  * @generated
  * @phutil-library-version 2
  */
 phutil_register_library_map(array(
   '__library_version__' => 2,
   'class' => array(
     'AASTNode' => 'parser/aast/api/AASTNode.php',
     'AASTNodeList' => 'parser/aast/api/AASTNodeList.php',
     'AASTToken' => 'parser/aast/api/AASTToken.php',
     'AASTTree' => 'parser/aast/api/AASTTree.php',
     'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php',
     'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php',
     'AphrontAccessDeniedQueryException' => 'aphront/storage/exception/AphrontAccessDeniedQueryException.php',
     'AphrontBaseMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php',
     'AphrontCharacterSetQueryException' => 'aphront/storage/exception/AphrontCharacterSetQueryException.php',
     'AphrontConnectionLostQueryException' => 'aphront/storage/exception/AphrontConnectionLostQueryException.php',
     'AphrontConnectionQueryException' => 'aphront/storage/exception/AphrontConnectionQueryException.php',
     'AphrontCountQueryException' => 'aphront/storage/exception/AphrontCountQueryException.php',
     'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php',
     'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php',
     'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php',
     'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php',
     'AphrontInvalidCredentialsQueryException' => 'aphront/storage/exception/AphrontInvalidCredentialsQueryException.php',
     'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php',
     'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php',
     'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
     'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
     'AphrontNotSupportedQueryException' => 'aphront/storage/exception/AphrontNotSupportedQueryException.php',
     'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php',
     'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php',
     'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php',
     'AphrontQueryTimeoutQueryException' => 'aphront/storage/exception/AphrontQueryTimeoutQueryException.php',
     'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php',
     'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php',
     'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php',
     'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
     'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
     'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
     'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php',
     'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php',
     'CommandException' => 'future/exec/CommandException.php',
     'ConduitClient' => 'conduit/ConduitClient.php',
     'ConduitClientException' => 'conduit/ConduitClientException.php',
     'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php',
     'ConduitFuture' => 'conduit/ConduitFuture.php',
     'ExecFuture' => 'future/exec/ExecFuture.php',
     'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
     'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php',
     'FileFinder' => 'filesystem/FileFinder.php',
     'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
     'FileList' => 'filesystem/FileList.php',
     'Filesystem' => 'filesystem/Filesystem.php',
     'FilesystemException' => 'filesystem/FilesystemException.php',
     'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php',
     'Future' => 'future/Future.php',
     'FutureIterator' => 'future/FutureIterator.php',
     'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php',
     'FutureProxy' => 'future/FutureProxy.php',
     'HTTPFuture' => 'future/http/HTTPFuture.php',
     'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php',
     'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php',
     'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php',
     'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php',
     'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php',
     'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php',
     'HTTPSFuture' => 'future/http/HTTPSFuture.php',
     'ImmediateFuture' => 'future/ImmediateFuture.php',
     'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.php',
     'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php',
     'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php',
     'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php',
     'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
     'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
     'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
     'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php',
     'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php',
     'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php',
     'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php',
     'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php',
     'Phobject' => 'object/Phobject.php',
     'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php',
     'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php',
     'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
     'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
     'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
     'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php',
     'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php',
     'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
     'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php',
     'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php',
     'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php',
     'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php',
     'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php',
     'PhutilAggregateException' => 'error/PhutilAggregateException.php',
     'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php',
     'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php',
     'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php',
     'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php',
     'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php',
     'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php',
     'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php',
     'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php',
     'PhutilArgumentSpellingCorrector' => 'parser/argument/PhutilArgumentSpellingCorrector.php',
     'PhutilArgumentSpellingCorrectorTestCase' => 'parser/argument/__tests__/PhutilArgumentSpellingCorrectorTestCase.php',
     'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
     'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
     'PhutilArray' => 'utils/PhutilArray.php',
     'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
     'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
     'PhutilAsanaAuthAdapter' => 'auth/PhutilAsanaAuthAdapter.php',
     'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
     'PhutilAuthAdapter' => 'auth/PhutilAuthAdapter.php',
     'PhutilAuthConfigurationException' => 'auth/exception/PhutilAuthConfigurationException.php',
     'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php',
     'PhutilAuthException' => 'auth/exception/PhutilAuthException.php',
     'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php',
     'PhutilBacktraceSignalHandler' => 'future/exec/PhutilBacktraceSignalHandler.php',
     'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php',
     'PhutilBitbucketAuthAdapter' => 'auth/PhutilBitbucketAuthAdapter.php',
     'PhutilBootloader' => 'moduleutils/PhutilBootloader.php',
     'PhutilBootloaderException' => 'moduleutils/PhutilBootloaderException.php',
     'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php',
     'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php',
     'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
     'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php',
     'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php',
     'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php',
     'PhutilCIDRList' => 'ip/PhutilCIDRList.php',
     'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php',
     'PhutilCalendarAbsoluteDateTime' => 'parser/calendar/data/PhutilCalendarAbsoluteDateTime.php',
     'PhutilCalendarContainerNode' => 'parser/calendar/data/PhutilCalendarContainerNode.php',
     'PhutilCalendarDateTime' => 'parser/calendar/data/PhutilCalendarDateTime.php',
     'PhutilCalendarDocumentNode' => 'parser/calendar/data/PhutilCalendarDocumentNode.php',
     'PhutilCalendarDuration' => 'parser/calendar/data/PhutilCalendarDuration.php',
     'PhutilCalendarEventNode' => 'parser/calendar/data/PhutilCalendarEventNode.php',
     'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php',
     'PhutilCalendarProxyDateTime' => 'parser/calendar/data/PhutilCalendarProxyDateTime.php',
     'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php',
     'PhutilCalendarRecurrenceList' => 'parser/calendar/data/PhutilCalendarRecurrenceList.php',
     'PhutilCalendarRecurrenceRule' => 'parser/calendar/data/PhutilCalendarRecurrenceRule.php',
+    'PhutilCalendarRecurrenceRuleTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php',
     'PhutilCalendarRecurrenceSet' => 'parser/calendar/data/PhutilCalendarRecurrenceSet.php',
     'PhutilCalendarRecurrenceSource' => 'parser/calendar/data/PhutilCalendarRecurrenceSource.php',
     'PhutilCalendarRecurrenceTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php',
     'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php',
     'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php',
     'PhutilCalendarUserNode' => 'parser/calendar/data/PhutilCalendarUserNode.php',
     'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
     'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php',
     'PhutilChannel' => 'channel/PhutilChannel.php',
     'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php',
     'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php',
     'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php',
     'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php',
     'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php',
     'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php',
     'PhutilCommandString' => 'xsprintf/PhutilCommandString.php',
     'PhutilConsole' => 'console/PhutilConsole.php',
     'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php',
     'PhutilConsoleConcatenatedView' => 'console/view/PhutilConsoleConcatenatedView.php',
     'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php',
     'PhutilConsoleList' => 'console/view/PhutilConsoleList.php',
     'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php',
     'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php',
     'PhutilConsoleServer' => 'console/PhutilConsoleServer.php',
     'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
     'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
     'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php',
     'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php',
     'PhutilConsoleView' => 'console/view/PhutilConsoleView.php',
     'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
     'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php',
     'PhutilCowsay' => 'utils/PhutilCowsay.php',
     'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php',
     'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php',
     'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php',
     'PhutilDaemon' => 'daemon/PhutilDaemon.php',
     'PhutilDaemonHandle' => 'daemon/PhutilDaemonHandle.php',
     'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php',
     'PhutilDaemonOverseerModule' => 'daemon/PhutilDaemonOverseerModule.php',
     'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php',
     'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php',
     'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php',
     'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php',
     'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php',
     'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php',
     'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php',
     'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php',
     'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php',
     'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.php',
     'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php',
     'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php',
     'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php',
     'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php',
     'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php',
     'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php',
     'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php',
     'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
     'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
     'PhutilEmojiLocale' => 'internationalization/locales/PhutilEmojiLocale.php',
     'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php',
     'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php',
     'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
     'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php',
     'PhutilErrorTrap' => 'error/PhutilErrorTrap.php',
     'PhutilEvent' => 'events/PhutilEvent.php',
     'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php',
     'PhutilEventEngine' => 'events/PhutilEventEngine.php',
     'PhutilEventListener' => 'events/PhutilEventListener.php',
     'PhutilEventType' => 'events/constant/PhutilEventType.php',
     'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php',
     'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php',
     'PhutilExecChannel' => 'channel/PhutilExecChannel.php',
     'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php',
     'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php',
     'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php',
     'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php',
     'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php',
     'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php',
     'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
     'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
     'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
     'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php',
     'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php',
     'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php',
     'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php',
     'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php',
     'PhutilGitURI' => 'parser/PhutilGitURI.php',
     'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
     'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php',
     'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php',
     'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
     'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php',
     'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php',
     'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
     'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php',
     'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php',
     'PhutilICSParser' => 'parser/calendar/ics/PhutilICSParser.php',
     'PhutilICSParserException' => 'parser/calendar/ics/PhutilICSParserException.php',
     'PhutilICSParserTestCase' => 'parser/calendar/ics/__tests__/PhutilICSParserTestCase.php',
     'PhutilICSWriter' => 'parser/calendar/ics/PhutilICSWriter.php',
     'PhutilICSWriterTestCase' => 'parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php',
     'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php',
     'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
     'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php',
     'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php',
     'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
     'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php',
     'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php',
     'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php',
     'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php',
     'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php',
     'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php',
     'PhutilJSON' => 'parser/PhutilJSON.php',
     'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php',
     'PhutilJSONFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php',
     'PhutilJSONParser' => 'parser/PhutilJSONParser.php',
     'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php',
     'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php',
     'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php',
     'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php',
     'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php',
     'PhutilJavaCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php',
     'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php',
     'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php',
     'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php',
     'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php',
     'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php',
     'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php',
     'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php',
     'PhutilLDAPAuthAdapter' => 'auth/PhutilLDAPAuthAdapter.php',
     'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php',
     'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php',
     'PhutilLexer' => 'lexer/PhutilLexer.php',
     'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php',
     'PhutilLibraryConflictException' => 'moduleutils/PhutilLibraryConflictException.php',
     'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php',
     'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php',
     'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php',
     'PhutilLocale' => 'internationalization/PhutilLocale.php',
     'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php',
     'PhutilLock' => 'filesystem/PhutilLock.php',
     'PhutilLockException' => 'filesystem/PhutilLockException.php',
     'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php',
     'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php',
     'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php',
     'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php',
     'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php',
     'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php',
     'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php',
     'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php',
     'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php',
     'PhutilModuleUtilsTestCase' => 'moduleutils/__tests__/PhutilModuleUtilsTestCase.php',
     'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php',
     'PhutilNumber' => 'internationalization/PhutilNumber.php',
     'PhutilOAuth1AuthAdapter' => 'auth/PhutilOAuth1AuthAdapter.php',
     'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php',
     'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php',
     'PhutilOAuthAuthAdapter' => 'auth/PhutilOAuthAuthAdapter.php',
     'PhutilOnDiskKeyValueCache' => 'cache/PhutilOnDiskKeyValueCache.php',
     'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php',
     'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php',
     'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php',
     'PhutilPHPCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php',
     'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php',
     'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php',
     'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php',
     'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php',
     'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php',
     'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php',
     'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php',
     'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php',
     'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php',
     'PhutilPerson' => 'internationalization/PhutilPerson.php',
     'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php',
     'PhutilPersonaAuthAdapter' => 'auth/PhutilPersonaAuthAdapter.php',
     'PhutilPhabricatorAuthAdapter' => 'auth/PhutilPhabricatorAuthAdapter.php',
     'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php',
     'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php',
     'PhutilPortugueseBrazilLocale' => 'internationalization/locales/PhutilPortugueseBrazilLocale.php',
     'PhutilPortuguesePortugalLocale' => 'internationalization/locales/PhutilPortuguesePortugalLocale.php',
     'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php',
     'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php',
     'PhutilProseDiff' => 'utils/PhutilProseDiff.php',
     'PhutilProseDiffTestCase' => 'utils/__tests__/PhutilProseDiffTestCase.php',
     'PhutilProseDifferenceEngine' => 'utils/PhutilProseDifferenceEngine.php',
     'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php',
     'PhutilProxyException' => 'error/PhutilProxyException.php',
     'PhutilProxyIterator' => 'utils/PhutilProxyIterator.php',
     'PhutilPygmentizeParser' => 'parser/PhutilPygmentizeParser.php',
     'PhutilPygmentizeParserTestCase' => 'parser/__tests__/PhutilPygmentizeParserTestCase.php',
     'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php',
     'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php',
     'PhutilQsprintfInterface' => 'xsprintf/PhutilQsprintfInterface.php',
     'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php',
     'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php',
     'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php',
     'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php',
     'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php',
     'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php',
     'PhutilRealNameContextFreeGrammar' => 'grammar/PhutilRealNameContextFreeGrammar.php',
     'PhutilRemarkupBlockInterpreter' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php',
     'PhutilRemarkupBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php',
     'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php',
     'PhutilRemarkupBoldRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php',
     'PhutilRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php',
     'PhutilRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php',
     'PhutilRemarkupDelRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php',
     'PhutilRemarkupDocumentLinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php',
     'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php',
     'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php',
     'PhutilRemarkupEscapeRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php',
     'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php',
     'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php',
     'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php',
     'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php',
     'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php',
     'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php',
     'PhutilRemarkupItalicRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php',
     'PhutilRemarkupLinebreaksRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php',
     'PhutilRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php',
     'PhutilRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php',
     'PhutilRemarkupMonospaceRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php',
     'PhutilRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php',
     'PhutilRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php',
     'PhutilRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php',
     'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php',
     'PhutilRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php',
     'PhutilRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php',
     'PhutilRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php',
     'PhutilRemarkupUnderlineRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php',
     'PhutilRope' => 'utils/PhutilRope.php',
     'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php',
     'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php',
     'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php',
     'PhutilSafeHTMLTestCase' => 'markup/__tests__/PhutilSafeHTMLTestCase.php',
     'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php',
     'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php',
     'PhutilShellLexer' => 'lexer/PhutilShellLexer.php',
     'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php',
     'PhutilSignalHandler' => 'future/exec/PhutilSignalHandler.php',
     'PhutilSignalRouter' => 'future/exec/PhutilSignalRouter.php',
     'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php',
     'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php',
     'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php',
     'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php',
     'PhutilSimplifiedChineseLocale' => 'internationalization/locales/PhutilSimplifiedChineseLocale.php',
     'PhutilSlackAuthAdapter' => 'auth/PhutilSlackAuthAdapter.php',
     'PhutilSlackFuture' => 'future/slack/PhutilSlackFuture.php',
     'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php',
     'PhutilSortVector' => 'utils/PhutilSortVector.php',
     'PhutilSpanishSpainLocale' => 'internationalization/locales/PhutilSpanishSpainLocale.php',
     'PhutilSprite' => 'sprites/PhutilSprite.php',
     'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php',
     'PhutilStreamIterator' => 'utils/PhutilStreamIterator.php',
     'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php',
     'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php',
     'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php',
     'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php',
     'PhutilSystem' => 'utils/PhutilSystem.php',
     'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php',
     'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php',
     'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php',
     'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php',
     'PhutilTraditionalChineseLocale' => 'internationalization/locales/PhutilTraditionalChineseLocale.php',
     'PhutilTranslation' => 'internationalization/PhutilTranslation.php',
     'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php',
     'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
     'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
     'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php',
     'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php',
     'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php',
     'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php',
     'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php',
     'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php',
     'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php',
     'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php',
     'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php',
     'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php',
     'PhutilURI' => 'parser/PhutilURI.php',
     'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php',
     'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
     'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
     'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
     'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
     'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
     'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php',
     'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php',
     'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php',
     'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php',
     'PhutilWordPressAuthAdapter' => 'auth/PhutilWordPressAuthAdapter.php',
     'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php',
     'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php',
     'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
     'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
     'QueryFuture' => 'future/query/QueryFuture.php',
     'TempFile' => 'filesystem/TempFile.php',
     'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
     'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
     'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php',
     'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php',
     'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php',
     'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php',
     'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php',
     'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php',
   ),
   'function' => array(
     'array_fuse' => 'utils/utils.php',
     'array_interleave' => 'utils/utils.php',
     'array_mergev' => 'utils/utils.php',
     'array_select_keys' => 'utils/utils.php',
     'assert_instances_of' => 'utils/utils.php',
     'assert_stringlike' => 'utils/utils.php',
     'coalesce' => 'utils/utils.php',
     'csprintf' => 'xsprintf/csprintf.php',
     'exec_manual' => 'future/exec/execx.php',
     'execx' => 'future/exec/execx.php',
     'head' => 'utils/utils.php',
     'head_key' => 'utils/utils.php',
     'hgsprintf' => 'xsprintf/hgsprintf.php',
     'hsprintf' => 'markup/render.php',
     'id' => 'utils/utils.php',
     'idx' => 'utils/utils.php',
     'idxv' => 'utils/utils.php',
     'ifilter' => 'utils/utils.php',
     'igroup' => 'utils/utils.php',
     'ipull' => 'utils/utils.php',
     'isort' => 'utils/utils.php',
     'jsprintf' => 'xsprintf/jsprintf.php',
     'last' => 'utils/utils.php',
     'last_key' => 'utils/utils.php',
     'ldap_sprintf' => 'xsprintf/ldapsprintf.php',
     'mfilter' => 'utils/utils.php',
     'mgroup' => 'utils/utils.php',
     'mpull' => 'utils/utils.php',
     'msort' => 'utils/utils.php',
     'msortv' => 'utils/utils.php',
     'newv' => 'utils/utils.php',
     'nonempty' => 'utils/utils.php',
     'phlog' => 'error/phlog.php',
     'pht' => 'internationalization/pht.php',
     'phutil_censor_credentials' => 'utils/utils.php',
     'phutil_console_confirm' => 'console/format.php',
     'phutil_console_format' => 'console/format.php',
     'phutil_console_get_terminal_width' => 'console/format.php',
     'phutil_console_prompt' => 'console/format.php',
     'phutil_console_require_tty' => 'console/format.php',
     'phutil_console_wrap' => 'console/format.php',
     'phutil_count' => 'internationalization/pht.php',
     'phutil_date_format' => 'utils/viewutils.php',
     'phutil_deprecated' => 'moduleutils/moduleutils.php',
     'phutil_error_listener_example' => 'error/phlog.php',
     'phutil_escape_html' => 'markup/render.php',
     'phutil_escape_html_newlines' => 'markup/render.php',
     'phutil_escape_uri' => 'markup/render.php',
     'phutil_escape_uri_path_component' => 'markup/render.php',
     'phutil_fnmatch' => 'utils/utils.php',
     'phutil_format_bytes' => 'utils/viewutils.php',
     'phutil_format_relative_time' => 'utils/viewutils.php',
     'phutil_format_relative_time_detailed' => 'utils/viewutils.php',
     'phutil_format_units_generic' => 'utils/viewutils.php',
     'phutil_fwrite_nonblocking_stream' => 'utils/utils.php',
     'phutil_get_current_library_name' => 'moduleutils/moduleutils.php',
     'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php',
     'phutil_get_library_root' => 'moduleutils/moduleutils.php',
     'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php',
     'phutil_get_signal_name' => 'future/exec/execx.php',
     'phutil_hashes_are_identical' => 'utils/utils.php',
     'phutil_implode_html' => 'markup/render.php',
     'phutil_ini_decode' => 'utils/utils.php',
     'phutil_is_hiphop_runtime' => 'utils/utils.php',
     'phutil_is_utf8' => 'utils/utf8.php',
     'phutil_is_utf8_slowly' => 'utils/utf8.php',
     'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php',
     'phutil_is_windows' => 'utils/utils.php',
     'phutil_json_decode' => 'utils/utils.php',
     'phutil_json_encode' => 'utils/utils.php',
     'phutil_load_library' => 'moduleutils/core.php',
     'phutil_loggable_string' => 'utils/utils.php',
     'phutil_parse_bytes' => 'utils/viewutils.php',
     'phutil_passthru' => 'future/exec/execx.php',
     'phutil_register_library' => 'moduleutils/core.php',
     'phutil_register_library_map' => 'moduleutils/core.php',
     'phutil_safe_html' => 'markup/render.php',
     'phutil_split_lines' => 'utils/utils.php',
     'phutil_tag' => 'markup/render.php',
     'phutil_tag_div' => 'markup/render.php',
     'phutil_unescape_uri_path_component' => 'markup/render.php',
     'phutil_units' => 'utils/utils.php',
     'phutil_utf8_console_strlen' => 'utils/utf8.php',
     'phutil_utf8_convert' => 'utils/utf8.php',
     'phutil_utf8_encode_codepoint' => 'utils/utf8.php',
     'phutil_utf8_hard_wrap' => 'utils/utf8.php',
     'phutil_utf8_hard_wrap_html' => 'utils/utf8.php',
     'phutil_utf8_is_combining_character' => 'utils/utf8.php',
     'phutil_utf8_strlen' => 'utils/utf8.php',
     'phutil_utf8_strtolower' => 'utils/utf8.php',
     'phutil_utf8_strtoupper' => 'utils/utf8.php',
     'phutil_utf8_strtr' => 'utils/utf8.php',
     'phutil_utf8_ucwords' => 'utils/utf8.php',
     'phutil_utf8ize' => 'utils/utf8.php',
     'phutil_utf8v' => 'utils/utf8.php',
     'phutil_utf8v_codepoints' => 'utils/utf8.php',
     'phutil_utf8v_combine_characters' => 'utils/utf8.php',
     'phutil_utf8v_combined' => 'utils/utf8.php',
     'phutil_validate_json' => 'utils/utils.php',
     'phutil_var_export' => 'utils/utils.php',
     'ppull' => 'utils/utils.php',
     'pregsprintf' => 'xsprintf/pregsprintf.php',
     'qsprintf' => 'xsprintf/qsprintf.php',
     'qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php',
     'qsprintf_check_type' => 'xsprintf/qsprintf.php',
     'queryfx' => 'xsprintf/queryfx.php',
     'queryfx_all' => 'xsprintf/queryfx.php',
     'queryfx_one' => 'xsprintf/queryfx.php',
     'tsprintf' => 'xsprintf/tsprintf.php',
     'urisprintf' => 'xsprintf/urisprintf.php',
     'vcsprintf' => 'xsprintf/csprintf.php',
     'vjsprintf' => 'xsprintf/jsprintf.php',
     'vqsprintf' => 'xsprintf/qsprintf.php',
     'vurisprintf' => 'xsprintf/urisprintf.php',
     'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php',
     'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php',
     'xsprintf' => 'xsprintf/xsprintf.php',
     'xsprintf_callback_example' => 'xsprintf/xsprintf.php',
     'xsprintf_command' => 'xsprintf/csprintf.php',
     'xsprintf_javascript' => 'xsprintf/jsprintf.php',
     'xsprintf_ldap' => 'xsprintf/ldapsprintf.php',
     'xsprintf_mercurial' => 'xsprintf/hgsprintf.php',
     'xsprintf_query' => 'xsprintf/qsprintf.php',
     'xsprintf_regex' => 'xsprintf/pregsprintf.php',
     'xsprintf_terminal' => 'xsprintf/tsprintf.php',
     'xsprintf_uri' => 'xsprintf/urisprintf.php',
   ),
   'xmap' => array(
     'AASTNode' => 'Phobject',
     'AASTNodeList' => array(
       'Phobject',
       'Countable',
       'Iterator',
     ),
     'AASTToken' => 'Phobject',
     'AASTTree' => 'Phobject',
     'AbstractDirectedGraph' => 'Phobject',
     'AbstractDirectedGraphTestCase' => 'PhutilTestCase',
     'AphrontAccessDeniedQueryException' => 'AphrontQueryException',
     'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontCharacterSetQueryException' => 'AphrontQueryException',
     'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException',
     'AphrontConnectionQueryException' => 'AphrontQueryException',
     'AphrontCountQueryException' => 'AphrontQueryException',
     'AphrontDatabaseConnection' => array(
       'Phobject',
       'PhutilQsprintfInterface',
     ),
     'AphrontDatabaseTransactionState' => 'Phobject',
     'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException',
     'AphrontDuplicateKeyQueryException' => 'AphrontQueryException',
     'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException',
     'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException',
     'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
     'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
     'AphrontNotSupportedQueryException' => 'AphrontQueryException',
     'AphrontObjectMissingQueryException' => 'AphrontQueryException',
     'AphrontParameterQueryException' => 'AphrontQueryException',
     'AphrontQueryException' => 'Exception',
     'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException',
     'AphrontRecoverableQueryException' => 'AphrontQueryException',
     'AphrontRequestStream' => 'Phobject',
     'AphrontSchemaQueryException' => 'AphrontQueryException',
     'AphrontScopedUnguardedWriteCapability' => 'Phobject',
     'AphrontWriteGuard' => 'Phobject',
     'BaseHTTPFuture' => 'Future',
     'CaseInsensitiveArray' => 'PhutilArray',
     'CaseInsensitiveArrayTestCase' => 'PhutilTestCase',
     'CommandException' => 'Exception',
     'ConduitClient' => 'Phobject',
     'ConduitClientException' => 'Exception',
     'ConduitClientTestCase' => 'PhutilTestCase',
     'ConduitFuture' => 'FutureProxy',
     'ExecFuture' => 'PhutilExecutableFuture',
     'ExecFutureTestCase' => 'PhutilTestCase',
     'ExecPassthruTestCase' => 'PhutilTestCase',
     'FileFinder' => 'Phobject',
     'FileFinderTestCase' => 'PhutilTestCase',
     'FileList' => 'Phobject',
     'Filesystem' => 'Phobject',
     'FilesystemException' => 'Exception',
     'FilesystemTestCase' => 'PhutilTestCase',
     'Future' => 'Phobject',
     'FutureIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'FutureIteratorTestCase' => 'PhutilTestCase',
     'FutureProxy' => 'Future',
     'HTTPFuture' => 'BaseHTTPFuture',
     'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPFutureResponseStatus' => 'Exception',
     'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus',
     'HTTPSFuture' => 'BaseHTTPFuture',
     'ImmediateFuture' => 'Future',
     'LibphutilUSEnglishTranslation' => 'PhutilTranslation',
     'LinesOfALarge' => array(
       'Phobject',
       'Iterator',
     ),
     'LinesOfALargeExecFuture' => 'LinesOfALarge',
     'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase',
     'LinesOfALargeFile' => 'LinesOfALarge',
     'LinesOfALargeFileTestCase' => 'PhutilTestCase',
     'MFilterTestHelper' => 'Phobject',
     'PHPASTParserTestCase' => 'PhutilTestCase',
     'PhageAgentBootloader' => 'Phobject',
     'PhageAgentTestCase' => 'PhutilTestCase',
     'PhagePHPAgent' => 'Phobject',
     'PhagePHPAgentBootloader' => 'PhageAgentBootloader',
     'Phobject' => 'Iterator',
     'PhobjectTestCase' => 'PhutilTestCase',
     'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilAWSEC2Future' => 'PhutilAWSFuture',
     'PhutilAWSException' => 'Exception',
     'PhutilAWSFuture' => 'FutureProxy',
     'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow',
     'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
     'PhutilAWSS3Future' => 'PhutilAWSFuture',
     'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
     'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow',
     'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
     'PhutilAWSv4Signature' => 'Phobject',
     'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
     'PhutilAggregateException' => 'Exception',
     'PhutilAllCapsEnglishLocale' => 'PhutilLocale',
     'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilArgumentParser' => 'Phobject',
     'PhutilArgumentParserException' => 'Exception',
     'PhutilArgumentParserTestCase' => 'PhutilTestCase',
     'PhutilArgumentSpecification' => 'Phobject',
     'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException',
     'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase',
     'PhutilArgumentSpellingCorrector' => 'Phobject',
     'PhutilArgumentSpellingCorrectorTestCase' => 'PhutilTestCase',
     'PhutilArgumentUsageException' => 'PhutilArgumentParserException',
     'PhutilArgumentWorkflow' => 'Phobject',
     'PhutilArray' => array(
       'Phobject',
       'Countable',
       'ArrayAccess',
       'Iterator',
     ),
     'PhutilArrayTestCase' => 'PhutilTestCase',
     'PhutilArrayWithDefaultValue' => 'PhutilArray',
     'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilAsanaFuture' => 'FutureProxy',
     'PhutilAuthAdapter' => 'Phobject',
     'PhutilAuthConfigurationException' => 'PhutilAuthException',
     'PhutilAuthCredentialException' => 'PhutilAuthException',
     'PhutilAuthException' => 'Exception',
     'PhutilAuthUserAbortedException' => 'PhutilAuthException',
     'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler',
     'PhutilBallOfPHP' => 'Phobject',
     'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilBootloaderException' => 'Exception',
     'PhutilBritishEnglishLocale' => 'PhutilLocale',
     'PhutilBufferedIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilBufferedIteratorTestCase' => 'PhutilTestCase',
     'PhutilBugtraqParser' => 'Phobject',
     'PhutilBugtraqParserTestCase' => 'PhutilTestCase',
     'PhutilCIDRBlock' => 'Phobject',
     'PhutilCIDRList' => 'Phobject',
     'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar',
     'PhutilCalendarAbsoluteDateTime' => 'PhutilCalendarDateTime',
     'PhutilCalendarContainerNode' => 'PhutilCalendarNode',
     'PhutilCalendarDateTime' => 'Phobject',
     'PhutilCalendarDocumentNode' => 'PhutilCalendarContainerNode',
     'PhutilCalendarDuration' => 'Phobject',
     'PhutilCalendarEventNode' => 'PhutilCalendarNode',
     'PhutilCalendarNode' => 'Phobject',
     'PhutilCalendarProxyDateTime' => 'PhutilCalendarDateTime',
     'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode',
     'PhutilCalendarRecurrenceList' => 'PhutilCalendarRecurrenceSource',
     'PhutilCalendarRecurrenceRule' => 'PhutilCalendarRecurrenceSource',
+    'PhutilCalendarRecurrenceRuleTestCase' => 'PhutilTestCase',
     'PhutilCalendarRecurrenceSet' => 'Phobject',
     'PhutilCalendarRecurrenceSource' => 'Phobject',
     'PhutilCalendarRecurrenceTestCase' => 'PhutilTestCase',
     'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime',
     'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode',
     'PhutilCalendarUserNode' => 'PhutilCalendarNode',
     'PhutilCallbackFilterIterator' => 'FilterIterator',
     'PhutilCallbackSignalHandler' => 'PhutilSignalHandler',
     'PhutilChannel' => 'Phobject',
     'PhutilChannelChannel' => 'PhutilChannel',
     'PhutilChannelTestCase' => 'PhutilTestCase',
     'PhutilChunkedIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
     'PhutilClassMapQuery' => 'Phobject',
     'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilCommandString' => 'Phobject',
     'PhutilConsole' => 'Phobject',
     'PhutilConsoleBlock' => 'PhutilConsoleView',
     'PhutilConsoleConcatenatedView' => 'PhutilConsoleView',
     'PhutilConsoleFormatter' => 'Phobject',
     'PhutilConsoleList' => 'PhutilConsoleView',
     'PhutilConsoleMessage' => 'Phobject',
     'PhutilConsoleProgressBar' => 'Phobject',
     'PhutilConsoleServer' => 'Phobject',
     'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
     'PhutilConsoleStdinNotInteractiveException' => 'Exception',
     'PhutilConsoleSyntaxHighlighter' => 'Phobject',
     'PhutilConsoleTable' => 'PhutilConsoleView',
     'PhutilConsoleView' => 'Phobject',
     'PhutilConsoleWrapTestCase' => 'PhutilTestCase',
     'PhutilContextFreeGrammar' => 'Phobject',
     'PhutilCowsay' => 'Phobject',
     'PhutilCowsayTestCase' => 'PhutilTestCase',
     'PhutilCsprintfTestCase' => 'PhutilTestCase',
     'PhutilCzechLocale' => 'PhutilLocale',
     'PhutilDaemon' => 'Phobject',
     'PhutilDaemonHandle' => 'Phobject',
     'PhutilDaemonOverseer' => 'Phobject',
     'PhutilDaemonOverseerModule' => 'Phobject',
     'PhutilDefaultSyntaxHighlighter' => 'Phobject',
     'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine',
     'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy',
     'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase',
     'PhutilDeferredLog' => 'Phobject',
     'PhutilDeferredLogTestCase' => 'PhutilTestCase',
     'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
     'PhutilDirectoryFixture' => 'Phobject',
     'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilDivinerSyntaxHighlighter' => 'Phobject',
     'PhutilDocblockParser' => 'Phobject',
     'PhutilDocblockParserTestCase' => 'PhutilTestCase',
     'PhutilEditDistanceMatrix' => 'Phobject',
     'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase',
     'PhutilEditorConfig' => 'Phobject',
     'PhutilEditorConfigTestCase' => 'PhutilTestCase',
     'PhutilEmailAddress' => 'Phobject',
     'PhutilEmailAddressTestCase' => 'PhutilTestCase',
     'PhutilEmojiLocale' => 'PhutilLocale',
     'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilEnglishCanadaLocale' => 'PhutilLocale',
     'PhutilErrorHandler' => 'Phobject',
     'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
     'PhutilErrorTrap' => 'Phobject',
     'PhutilEvent' => 'Phobject',
     'PhutilEventConstants' => 'Phobject',
     'PhutilEventEngine' => 'Phobject',
     'PhutilEventListener' => 'Phobject',
     'PhutilEventType' => 'PhutilEventConstants',
     'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator',
     'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon',
     'PhutilExecChannel' => 'PhutilChannel',
     'PhutilExecPassthru' => 'PhutilExecutableFuture',
     'PhutilExecutableFuture' => 'Future',
     'PhutilExecutionEnvironment' => 'Phobject',
     'PhutilExtensionsTestCase' => 'PhutilTestCase',
     'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilFileLock' => 'PhutilLock',
     'PhutilFileLockTestCase' => 'PhutilTestCase',
     'PhutilFileTree' => 'Phobject',
     'PhutilFrenchLocale' => 'PhutilLocale',
     'PhutilGermanLocale' => 'PhutilLocale',
     'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilGitHubFuture' => 'FutureProxy',
     'PhutilGitHubResponse' => 'Phobject',
     'PhutilGitURI' => 'Phobject',
     'PhutilGitURITestCase' => 'PhutilTestCase',
     'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilHTTPEngineExtension' => 'Phobject',
     'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
     'PhutilHashingIterator' => array(
       'PhutilProxyIterator',
       'Iterator',
     ),
     'PhutilHashingIteratorTestCase' => 'PhutilTestCase',
     'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
     'PhutilHgsprintfTestCase' => 'PhutilTestCase',
     'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilICSParser' => 'Phobject',
     'PhutilICSParserException' => 'Exception',
     'PhutilICSParserTestCase' => 'PhutilTestCase',
     'PhutilICSWriter' => 'Phobject',
     'PhutilICSWriterTestCase' => 'PhutilTestCase',
     'PhutilINIParserException' => 'Exception',
     'PhutilIPAddress' => 'Phobject',
     'PhutilIPAddressTestCase' => 'PhutilTestCase',
     'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilInteractiveEditor' => 'Phobject',
     'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilInvalidStateException' => 'Exception',
     'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase',
     'PhutilInvisibleSyntaxHighlighter' => 'Phobject',
     'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilJSON' => 'Phobject',
     'PhutilJSONFragmentLexer' => 'PhutilLexer',
     'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
     'PhutilJSONParser' => 'Phobject',
     'PhutilJSONParserException' => 'Exception',
     'PhutilJSONParserTestCase' => 'PhutilTestCase',
     'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
     'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase',
     'PhutilJSONTestCase' => 'PhutilTestCase',
     'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
     'PhutilKeyValueCache' => 'Phobject',
     'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy',
     'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy',
     'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache',
     'PhutilKeyValueCacheTestCase' => 'PhutilTestCase',
     'PhutilKoreanLocale' => 'PhutilLocale',
     'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilLanguageGuesser' => 'Phobject',
     'PhutilLanguageGuesserTestCase' => 'PhutilTestCase',
     'PhutilLexer' => 'Phobject',
     'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter',
     'PhutilLibraryConflictException' => 'Exception',
     'PhutilLibraryMapBuilder' => 'Phobject',
     'PhutilLibraryTestCase' => 'PhutilTestCase',
     'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilLocale' => 'Phobject',
     'PhutilLocaleTestCase' => 'PhutilTestCase',
     'PhutilLock' => 'Phobject',
     'PhutilLockException' => 'Exception',
     'PhutilLogFileChannel' => 'PhutilChannelChannel',
     'PhutilLunarPhase' => 'Phobject',
     'PhutilLunarPhaseTestCase' => 'PhutilTestCase',
     'PhutilMarkupEngine' => 'Phobject',
     'PhutilMarkupTestCase' => 'PhutilTestCase',
     'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilMethodNotImplementedException' => 'Exception',
     'PhutilMetricsChannel' => 'PhutilChannelChannel',
     'PhutilMissingSymbolException' => 'Exception',
     'PhutilModuleUtilsTestCase' => 'PhutilTestCase',
     'PhutilNiceDaemon' => 'PhutilTortureTestDaemon',
     'PhutilNumber' => 'Phobject',
     'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter',
     'PhutilOAuth1Future' => 'FutureProxy',
     'PhutilOAuth1FutureTestCase' => 'PhutilTestCase',
     'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilOpaqueEnvelope' => 'Phobject',
     'PhutilOpaqueEnvelopeKey' => 'Phobject',
     'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase',
     'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
     'PhutilPHPFragmentLexer' => 'PhutilLexer',
     'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
     'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase',
     'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel',
     'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase',
     'PhutilParserGenerator' => 'Phobject',
     'PhutilParserGeneratorException' => 'Exception',
     'PhutilParserGeneratorTestCase' => 'PhutilTestCase',
     'PhutilPayPalAPIFuture' => 'FutureProxy',
     'PhutilPersonTest' => array(
       'Phobject',
       'PhutilPerson',
     ),
     'PhutilPersonaAuthAdapter' => 'PhutilAuthAdapter',
     'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilPhtTestCase' => 'PhutilTestCase',
     'PhutilPirateEnglishLocale' => 'PhutilLocale',
     'PhutilPortugueseBrazilLocale' => 'PhutilLocale',
     'PhutilPortuguesePortugalLocale' => 'PhutilLocale',
     'PhutilPregsprintfTestCase' => 'PhutilTestCase',
     'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon',
     'PhutilProseDiff' => 'Phobject',
     'PhutilProseDiffTestCase' => 'PhutilTestCase',
     'PhutilProseDifferenceEngine' => 'Phobject',
     'PhutilProtocolChannel' => 'PhutilChannelChannel',
     'PhutilProxyException' => 'Exception',
     'PhutilProxyIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilPygmentizeParser' => 'Phobject',
     'PhutilPygmentizeParserTestCase' => 'PhutilTestCase',
     'PhutilPygmentsSyntaxHighlighter' => 'Phobject',
     'PhutilPythonFragmentLexer' => 'PhutilLexer',
     'PhutilQueryStringParser' => 'Phobject',
     'PhutilQueryStringParserTestCase' => 'PhutilTestCase',
     'PhutilRainbowSyntaxHighlighter' => 'Phobject',
     'PhutilRawEnglishLocale' => 'PhutilLocale',
     'PhutilReadableSerializer' => 'Phobject',
     'PhutilReadableSerializerTestCase' => 'PhutilTestCase',
     'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
     'PhutilRemarkupBlockInterpreter' => 'Phobject',
     'PhutilRemarkupBlockRule' => 'Phobject',
     'PhutilRemarkupBlockStorage' => 'Phobject',
     'PhutilRemarkupBoldRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupCodeBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupDefaultBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupDelRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupDocumentLinkRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
     'PhutilRemarkupEngineTestCase' => 'PhutilTestCase',
     'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule',
     'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupRule' => 'Phobject',
     'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule',
     'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter',
     'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule',
     'PhutilRope' => 'Phobject',
     'PhutilRopeTestCase' => 'PhutilTestCase',
     'PhutilSafeHTML' => 'Phobject',
     'PhutilSafeHTMLTestCase' => 'PhutilTestCase',
     'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon',
     'PhutilServiceProfiler' => 'Phobject',
     'PhutilShellLexer' => 'PhutilLexer',
     'PhutilShellLexerTestCase' => 'PhutilTestCase',
     'PhutilSignalHandler' => 'Phobject',
     'PhutilSignalRouter' => 'Phobject',
     'PhutilSimpleOptions' => 'Phobject',
     'PhutilSimpleOptionsLexer' => 'PhutilLexer',
     'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase',
     'PhutilSimpleOptionsTestCase' => 'PhutilTestCase',
     'PhutilSimplifiedChineseLocale' => 'PhutilLocale',
     'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilSlackFuture' => 'FutureProxy',
     'PhutilSocketChannel' => 'PhutilChannel',
     'PhutilSortVector' => 'Phobject',
     'PhutilSpanishSpainLocale' => 'PhutilLocale',
     'PhutilSprite' => 'Phobject',
     'PhutilSpriteSheet' => 'Phobject',
     'PhutilStreamIterator' => array(
       'Phobject',
       'Iterator',
     ),
     'PhutilSyntaxHighlighter' => 'Phobject',
     'PhutilSyntaxHighlighterEngine' => 'Phobject',
     'PhutilSyntaxHighlighterException' => 'Exception',
     'PhutilSystem' => 'Phobject',
     'PhutilSystemTestCase' => 'PhutilTestCase',
     'PhutilTerminalString' => 'Phobject',
     'PhutilTestPhobject' => 'Phobject',
     'PhutilTortureTestDaemon' => 'PhutilDaemon',
     'PhutilTraditionalChineseLocale' => 'PhutilLocale',
     'PhutilTranslation' => 'Phobject',
     'PhutilTranslationTestCase' => 'PhutilTestCase',
     'PhutilTranslator' => 'Phobject',
     'PhutilTranslatorTestCase' => 'PhutilTestCase',
     'PhutilTsprintfTestCase' => 'PhutilTestCase',
     'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilTwitchFuture' => 'FutureProxy',
     'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
     'PhutilTypeCheckException' => 'Exception',
     'PhutilTypeExtraParametersException' => 'Exception',
     'PhutilTypeLexer' => 'PhutilLexer',
     'PhutilTypeMissingParametersException' => 'Exception',
     'PhutilTypeSpec' => 'Phobject',
     'PhutilTypeSpecTestCase' => 'PhutilTestCase',
     'PhutilURI' => 'Phobject',
     'PhutilURITestCase' => 'PhutilTestCase',
     'PhutilUSEnglishLocale' => 'PhutilLocale',
     'PhutilUTF8StringTruncator' => 'Phobject',
     'PhutilUTF8TestCase' => 'PhutilTestCase',
     'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException',
     'PhutilUrisprintfTestCase' => 'PhutilTestCase',
     'PhutilUtilsTestCase' => 'PhutilTestCase',
     'PhutilVeryWowEnglishLocale' => 'PhutilLocale',
     'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilWordPressFuture' => 'FutureProxy',
     'PhutilXHPASTBinary' => 'Phobject',
     'PhutilXHPASTSyntaxHighlighter' => 'Phobject',
     'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase',
     'QueryFuture' => 'Future',
     'TempFile' => 'Phobject',
     'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
     'XHPASTNode' => 'AASTNode',
     'XHPASTNodeTestCase' => 'PhutilTestCase',
     'XHPASTSyntaxErrorException' => 'Exception',
     'XHPASTToken' => 'AASTToken',
     'XHPASTTree' => 'AASTTree',
     'XHPASTTreeTestCase' => 'PhutilTestCase',
     'XsprintfUnknownConversionException' => 'InvalidArgumentException',
   ),
 ));
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
index 9754c6d..bca7f3b 100644
--- a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
+++ b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
@@ -1,142 +1,819 @@
 <?php
 
 final class PhutilCalendarRecurrenceRule
   extends PhutilCalendarRecurrenceSource {
 
+  private $startDateTime;
   private $frequency;
   private $until;
   private $count;
-  private $interval;
+  private $interval = 1;
   private $bySecond;
   private $byMinute;
   private $byHour;
   private $byDay;
   private $byMonthDay;
   private $byYearDay;
   private $byWeekNumber;
   private $byMonth;
   private $bySetPosition;
   private $weekStart = 'MO';
 
+  private $cursorSecond;
+  private $cursorMinute;
+  private $cursorHour;
+  private $cursorWeek;
+  private $cursorDay;
+  private $cursorMonth;
+  private $cursorYear;
+
+  private $setSeconds;
+  private $setMinutes;
+  private $setHours;
+  private $setDays;
+  private $setMonths;
+  private $setYears;
+
+  private $stateSecond;
+  private $stateMinute;
+  private $stateHour;
+  private $stateDay;
+  private $stateMonth;
+  private $stateYear;
+
+  private $maps = array();
+
+  public function setStartDateTime(PhutilCalendarDateTime $start) {
+    $this->startDateTime = $start;
+    return $this;
+  }
+
+  public function getStartDateTime() {
+    return $this->startDateTime;
+  }
+
   public function setFrequency($frequency) {
     $this->frequency = $frequency;
     return $this;
   }
 
   public function getFrequency() {
     return $this->frequency;
   }
 
   public function setUntil(PhutilCalendarDateTime $until) {
     $this->until = $until;
     return $this;
   }
 
   public function getUntil() {
     return $this->until;
   }
 
   public function setCount($count) {
     $this->count = $count;
     return $this;
   }
 
   public function getCount() {
     return $this->count;
   }
 
   public function setInterval($interval) {
     $this->interval = $interval;
     return $this;
   }
 
   public function getInterval() {
     return $this->interval;
   }
 
   public function setBySecond($by_second) {
     $this->bySecond = $by_second;
     return $this;
   }
 
   public function getBySecond() {
     return $this->bySecond;
   }
 
   public function setByMinute($by_minute) {
     $this->byMinute = $by_minute;
     return $this;
   }
 
   public function getByMinute() {
     return $this->byMinute;
   }
 
   public function setByHour($by_hour) {
     $this->byHour = $by_hour;
     return $this;
   }
 
   public function getByHour() {
     return $this->byHour;
   }
 
   public function setByDay($by_day) {
     $this->byDay = $by_day;
     return $this;
   }
 
   public function getByDay() {
     return $this->byDay;
   }
 
   public function setByMonthDay($by_month_day) {
     $this->byMonthDay = $by_month_day;
     return $this;
   }
 
   public function getByMonthDay() {
     return $this->byMonthDay;
   }
 
   public function setByYearDay($by_year_day) {
     $this->byYearDay = $by_year_day;
     return $this;
   }
 
   public function getByYearDay() {
     return $this->byYearDay;
   }
 
   public function setByMonth($by_month) {
     $this->byMonth = $by_month;
     return $this;
   }
 
   public function getByMonth() {
     return $this->byMonth;
   }
 
+  public function setByWeekNumber($by_week_number) {
+    $this->byWeekNumber = $by_week_number;
+    return $this;
+  }
+
+  public function getByWeekNumber() {
+    return $this->byWeekNumber;
+  }
+
   public function setBySetPosition($by_set_position) {
     $this->bySetPosition = $by_set_position;
     return $this;
   }
 
   public function getBySetPosition() {
     return $this->bySetPosition;
   }
 
   public function setWeekStart($week_start) {
     $this->weekStart = $week_start;
     return $this;
   }
 
   public function getWeekStart() {
     return $this->weekStart;
   }
 
+
+  public function resetSource() {
+    $date = $this->getStartDateTime();
+
+    $this->cursorSecond = $date->getSecond();
+    $this->cursorMinute = $date->getMinute();
+    $this->cursorHour = $date->getHour();
+
+    // TODO: Figure this out.
+    $this->cursorWeek = null;
+    $this->cursorDay = $date->getDay();
+    $this->cursorMonth = $date->getMonth();
+    $this->cursorYear = $date->getYear();
+
+    $this->setSeconds = array();
+    $this->setMinutes = array();
+    $this->setHours = array();
+    $this->setDays = array();
+    $this->setMonths = array();
+    $this->setYears = array();
+
+    $this->stateSecond = null;
+    $this->stateMinute = null;
+    $this->stateHour = null;
+    $this->stateDay = null;
+    $this->stateMonth = null;
+    $this->stateYear = null;
+  }
+
   public function getNextEvent($cursor) {
-    throw new PhutilMethodNotImplementedException();
+    $date = $this->getStartDateTime();
+
+    $all_day = $date->getIsAllDay();
+    if ($all_day) {
+      $this->nextDay();
+    } else {
+      $this->nextSecond();
+    }
+
+    $result = id(new PhutilCalendarAbsoluteDateTime())
+      ->setViewerTimezone($this->getViewerTimezone())
+      ->setYear($this->stateYear)
+      ->setMonth($this->stateMonth)
+      ->setDay($this->stateDay);
+
+    if ($all_day) {
+      $result->setIsAllDay(true);
+    } else {
+      $result
+        ->setHour($this->stateHour)
+        ->setMinute($this->stateMinute)
+        ->setSecond($this->stateSecond);
+    }
+
+    return $result;
   }
 
+
+  protected function nextSecond() {
+    if ($this->setSeconds) {
+      $this->stateSecond = array_pop($this->setSeconds);
+      return;
+    }
+
+    $frequency = $this->getFrequency();
+    $interval = $this->getInterval();
+    $is_secondly = ($frequency == 'SECONDLY');
+    $by_second = $this->getBySecond();
+    $by_setpos = $this->getBySetPosition();
+
+    while (!$this->setSeconds) {
+      $this->nextMinute();
+
+      if ($is_secondly || $by_second) {
+        $seconds = $this->newSecondsSet(
+          ($is_secondly ? $interval : 1),
+          $by_second);
+      } else {
+        $seconds = array(
+          $this->cursorSecond,
+        );
+      }
+
+      if ($is_secondly && $by_setpos) {
+        $seconds = $this->applySetPos($seconds, $by_setpos);
+      }
+
+      $this->setSeconds = array_reverse($seconds);
+    }
+
+    $this->stateSecond = array_pop($this->setSeconds);
+  }
+
+  protected function nextMinute() {
+    if ($this->setMinutes) {
+      $this->stateMinute = array_pop($this->setMinutes);
+      return;
+    }
+
+    $frequency = $this->getFrequency();
+    $interval = $this->getInterval();
+    $is_secondly = ($frequency === 'SECONDLY');
+    $is_minutely = ($frequency === 'MINUTELY');
+    $by_minute = $this->getByMinute();
+    $by_setpos = $this->getBySetPosition();
+
+    while (!$this->setMinutes) {
+      $this->nextHour();
+
+      if ($is_minutely || $by_minute) {
+        $minutes = $this->newMinutesSet(
+          ($is_minutely ? $interval : 1),
+          $by_minute);
+      } else if ($is_secondly) {
+        $minutes = $this->newMinutesSet(
+          1,
+          array());
+      } else {
+        $minutes = array(
+          $this->cursorMinute,
+        );
+      }
+
+      if ($is_minutely && $by_setpos) {
+        $minutes = $this->applySetPos($minutes, $by_setpos);
+      }
+
+      $this->setMinutes = array_reverse($minutes);
+    }
+
+    $this->stateMinute = array_pop($this->setMinutes);
+  }
+
+  protected function nextHour() {
+    if ($this->setHours) {
+      $this->stateHour = array_pop($this->setHours);
+      return;
+    }
+
+    $frequency = $this->getFrequency();
+    $interval = $this->getInterval();
+    $is_secondly = ($frequency === 'SECONDLY');
+    $is_minutely = ($frequency === 'MINUTELY');
+    $is_hourly = ($frequency === 'HOURLY');
+    $by_hour = $this->getByHour();
+    $by_setpos = $this->getBySetPosition();
+
+    while (!$this->setHours) {
+      $this->nextDay();
+
+      if ($is_minutely || $by_hour) {
+        $hours = $this->newHoursSet(
+          ($is_hourly ? $interval : 1),
+          $by_hour);
+      } else if ($is_secondly || $is_minutely) {
+        $hours = $this->newHoursSet(
+          1,
+          array());
+      } else {
+        $hours = array(
+          $this->cursorHour,
+        );
+      }
+
+      if ($is_hourly && $by_setpos) {
+        $hours = $this->applySetPos($hours, $by_setpos);
+      }
+
+      $this->setHours = array_reverse($hours);
+    }
+
+    $this->stateHour = array_pop($this->setHours);
+  }
+
+  protected function nextDay() {
+    if ($this->setDays) {
+      $this->stateDay = array_pop($this->setDays);
+      return;
+    }
+
+    $frequency = $this->getFrequency();
+    $interval = $this->getInterval();
+    $is_secondly = ($frequency === 'SECONDLY');
+    $is_minutely = ($frequency === 'MINUTELY');
+    $is_hourly = ($frequency === 'HOURLY');
+    $is_daily = ($frequency === 'DAILY');
+    $is_weekly = ($frequency === 'WEEKLY');
+
+    $by_day = $this->getByDay();
+    $by_monthday = $this->getByMonthDay();
+    $by_yearday = $this->getByYearDay();
+    $by_weekno = $this->getByWeekNumber();
+    $by_setpos = $this->getBySetPosition();
+
+    while (!$this->setDays) {
+      $this->nextMonth();
+
+      if ($is_daily || $by_day || $by_monthday || $by_yearday || $by_weekno) {
+        $weeks = $this->newDaysSet(
+          ($is_daily ? $interval : null),
+          ($is_weekly ? $interval : null),
+          $by_day,
+          $by_monthday,
+          $by_yearday,
+          $by_weekno,
+          $this->getWeekStart());
+      } else if ($is_secondly || $is_minutely || $is_hourly) {
+        $all_values = true;
+        $weeks = $this->newDaysSet(
+          1,
+          null,
+          array(),
+          array(),
+          array(),
+          array(),
+          $this->getWeekStart());
+      } else {
+        $weeks = array(
+          array($this->cursorDay),
+        );
+      }
+
+      // Apply weekly BYSETPOS, if one exists.
+      if ($is_weekly && $by_setpos) {
+        $weeks = $this->applySetPos($weeks, $by_setpos);
+      }
+
+      // Unpack the weeks into days.
+      $days = array_mergev($weeks);
+
+      // Apply daily BYSETPOS, if one exists.
+      if ($is_daily && $by_setpos) {
+        $days = $this->applySetPos($days, $by_setpos);
+      }
+
+      $this->setDays = array_reverse($days);
+    }
+
+    $this->stateDay = array_pop($this->setDays);
+  }
+
+  protected function nextMonth() {
+    if ($this->setMonths) {
+      $this->stateMonth = array_pop($this->setMonths);
+      return;
+    }
+
+    $frequency = $this->getFrequency();
+    $interval = $this->getInterval();
+    $is_secondly = ($frequency === 'SECONDLY');
+    $is_minutely = ($frequency === 'MINUTELY');
+    $is_hourly = ($frequency === 'HOURLY');
+    $is_daily = ($frequency === 'DAILY');
+    $is_weekly = ($frequency === 'WEEKLY');
+    $is_monthly = ($frequency === 'MONTHLY');
+
+    $by_month = $this->getByMonth();
+    $by_setpos = $this->getBySetPosition();
+
+    while (!$this->setMonths) {
+      $this->nextYear();
+
+      if ($is_monthly || $by_month) {
+        $months = $this->newMonthsSet(
+          ($is_monthly ? $interval : 1),
+          $by_month);
+      } else if (
+          $is_secondly || $is_minutely || $is_hourly ||
+          $is_daily || $is_weekly) {
+        $months = $this->newMonthsSet(
+          1,
+          array());
+      } else {
+        $months = array(
+          $this->cursorMonth,
+        );
+      }
+
+      if ($is_monthly && $by_setpos) {
+        $months = $this->applySetPos($months, $by_setpos);
+      }
+
+      $this->setMonths = array_reverse($months);
+    }
+
+    $this->stateMonth = array_pop($this->setMonths);
+  }
+
+  protected function nextYear() {
+    $this->stateYear = $this->cursorYear;
+
+    $frequency = $this->getFrequency();
+    $is_yearly = ($frequency === 'YEARLY');
+
+    if ($is_yearly) {
+      $interval = $this->getInterval();
+    } else {
+      $interval = 1;
+    }
+
+    $this->cursorYear = $this->cursorYear + $interval;
+  }
+
+  private function newSecondsSet($interval, $set) {
+    // TODO: This doesn't account for leap sections. In theory, it probably
+    // should, although this shouldn't impact any real events.
+    $seconds_in_minute = 60;
+
+    if ($this->cursorSecond >= $seconds_in_minute) {
+      $this->cursorSecond -= $seconds_in_minute;
+      return array();
+    }
+
+    list($cursor, $result) = $this->newIteratorSet(
+      $this->cursorSecond,
+      $interval,
+      $set,
+      $seconds_in_minute);
+
+    $this->cursorSecond = ($cursor - $seconds_in_minute);
+
+    return $result;
+  }
+
+  private function newMinutesSet($interval, $set) {
+    // NOTE: This value is legitimately a constant! Amazing!
+    $minutes_in_hour = 60;
+
+    if ($this->cursorMinute >= $minutes_in_hour) {
+      $this->cursorMinute -= $minutes_in_hour;
+      return array();
+    }
+
+    list($cursor, $result) = $this->newIteratorSet(
+      $this->cursorMinute,
+      $interval,
+      $set,
+      $minutes_in_hour);
+
+    $this->cursorMinute = ($cursor - $minutes_in_hour);
+
+    return $result;
+  }
+
+  private function newHoursSet($interval, $set) {
+    // TODO: This doesn't account for hours caused by daylight savings time.
+    // It probably should, although this seems unlikely to impact any real
+    // events.
+    $hours_in_day = 24;
+
+    if ($this->cursorHour >= $hours_in_day) {
+      $this->cursorHour -= $hours_in_day;
+      return array();
+    }
+
+    list($cursor, $result) = $this->newIteratorSet(
+      $this->cursorHour,
+      $interval,
+      $set,
+      $hours_in_day);
+
+    $this->cursorHour = ($cursor - $hours_in_day);
+
+    return $result;
+  }
+
+  private function newDaysSet(
+    $interval_day,
+    $interval_week,
+    $by_day,
+    $by_monthday,
+    $by_yearday,
+    $by_weekno,
+    $week_start) {
+
+    $year_map = $this->getYearMap($this->cursorYear, $week_start);
+
+    $selection = array();
+    if ($interval_week) {
+      while (true) {
+        // TODO: This is all garbage?
+        if ($this->cursorWeek > $year_map['weekCount']) {
+          $this->cursorWeek -= $year_map['weekCount'];
+          break;
+        }
+
+        foreach ($year_map['weeks'][$this->cursorWeek] as $key) {
+          $selection[] = $year_map['info'][$key];
+        }
+
+        $last = last($selection);
+        if ($last['month'] > $this->cursorMonth) {
+          break;
+        }
+
+        $this->cursorWeek += $interval_week;
+      }
+    } else {
+      $calendar = $year_map['calendar'];
+      $month_idx = $this->cursorMonth;
+
+      if (!$interval_day) {
+        $interval_day = 1;
+      }
+
+      while (true) {
+        $month_days = $year_map['monthDays'][$month_idx];
+        if ($this->cursorDay > $month_days) {
+          $this->cursorDay -= $month_days;
+          break;
+        }
+
+        $day_idx = $this->cursorDay;
+
+        $key = $calendar[$month_idx][$day_idx]['key'];
+        $selection[] = $year_map['info'][$key];
+
+        $this->cursorDay += $interval_day;
+      }
+    }
+
+    if ($by_day) {
+      $by_day = array_fuse($by_day);
+    }
+
+    if ($by_monthday) {
+      $by_monthday = array_fuse($by_monthday);
+    }
+
+    if ($by_yearday) {
+      $by_yearday = array_fuse($by_yearday);
+    }
+
+    if ($by_weekno) {
+      $by_weekno = array_fuse($by_weekno);
+    }
+
+    $weeks = array();
+    foreach ($selection as $key => $info) {
+      if ($info['month'] != $this->cursorMonth) {
+        continue;
+      }
+
+      if ($by_day) {
+        // TODO: Implement weekday stuff.
+      }
+
+      if ($by_monthday) {
+        if (empty($by_monthday[$info['monthday']]) &&
+            empty($by_monthday[$info['-monthday']])) {
+          continue;
+        }
+      }
+
+      if ($by_yearday) {
+        if (empty($by_monthday[$info['yearday']]) &&
+            empty($by_monthday[$info['-yearday']])) {
+          continue;
+        }
+      }
+
+      if ($by_weekno) {
+        // TODO: Implement week number stuff.
+      }
+
+      $weeks[$info['week']][] = $info['monthday'];
+    }
+
+    return array_values($weeks);
+  }
+
+  private function newMonthsSet($interval, $set) {
+    // NOTE: This value is also a real constant! Wow!
+    $months_in_year = 12;
+
+    if ($this->cursorMonth > $months_in_year) {
+      $this->cursorMonth - $months_in_year;
+      return array();
+    }
+
+    list($cursor, $result) = $this->newIteratorSet(
+      $this->cursorMonth,
+      $interval,
+      $set,
+      $months_in_year + 1);
+
+    $this->cursorMonth = ($cursor - $months_in_year);
+
+    return $result;
+  }
+
+  private function getYearMap($year, $week_start) {
+    $key = "{$year}/{$week_start}";
+    if (isset($this->maps[$key])) {
+      return $this->maps[$key];
+    }
+
+    $map = self::newYearMap($year, $week_start);
+    $this->maps[$key] = $map;
+
+    return $this->maps[$key];
+  }
+
+  public static function newYearMap($year, $week_start) {
+    $is_leap = (($year % 4 === 0) && ($year % 100 !== 0)) ||
+               ($year % 400 === 0);
+
+    // There may be some clever way to figure out which day of the week a given
+    // year starts on and avoid the cost of a DateTime construction, but I
+    // wasn't able to turn it up and we only need to do this once per year.
+    $datetime = new DateTime("{$year}-01-01", new DateTimeZone('UTC'));
+    $weekday = $datetime->format('w');
+
+    // TODO: Week 1 must contain at least 4 days!
+
+    if ($is_leap) {
+      $max_day = 366;
+    } else {
+      $max_day = 365;
+    }
+
+    $month_days = array(
+      1 => 31,
+      2 => $is_leap ? 29 : 28,
+      3 => 31,
+      4 => 30,
+      5 => 31,
+      6 => 30,
+      7 => 31,
+      8 => 31,
+      9 => 30,
+      10 => 31,
+      11 => 30,
+      12 => 31,
+    );
+
+    $info_map = array();
+    $calendar_map = array();
+    $week_map = array();
+    $yearday_map = array();
+
+    $month_number = 1;
+    $month_day = 1;
+    $week_number = 1;
+    for ($year_day = 1; $year_day <= $max_day; $year_day++) {
+      $key = $month_number.'/'.$month_day;
+
+      $info = array(
+        'key' => $key,
+        'month' => $month_number,
+        'monthday' => $month_day,
+        '-monthday' => -$month_days[$month_number] + $month_day - 1,
+        'yearday' => $year_day,
+        '-yearday' => -$max_day + $year_day - 1,
+        'week' => $week_number,
+      );
+
+      $info_map[$key] = $info;
+      $calendar_map[$month_number][$month_day] = $info;
+      $week_map[$week_number][] = $info;
+      $yearday_map[$year_day] = $info;
+
+      $weekday = ($weekday + 1) % 7;
+      if ($weekday === $week_start) {
+        $week_number++;
+      }
+
+      $month_day = ($month_day + 1);
+      if ($month_day > $month_days[$month_number]) {
+        $month_day = 1;
+        $month_number++;
+      }
+    }
+
+    return array(
+      'info' => $info_map,
+      'calendar' => $calendar_map,
+      'weeks' => $week_map,
+      'yeardays' => $yearday_map,
+      'weekCount' => $week_number,
+      'dayCount' => $max_day,
+      'monthDays' => $month_days,
+    );
+  }
+
+  private function newIteratorSet($cursor, $interval, $set, $limit) {
+    if ($interval < 1) {
+      throw new Exception(
+        pht(
+          'Invalid iteration interval ("%d"), must be at least 1.',
+          $interval));
+    }
+
+    if ($set) {
+      $set = array_fuse($set);
+    } else {
+      $set = array();
+    }
+
+    $result = array();
+    $seen = array();
+
+    $ii = $cursor;
+    while (true) {
+      if (!$set || isset($set[$ii])) {
+        $result[] = $ii;
+      }
+
+      $ii = ($ii + $interval);
+
+      if ($ii >= $limit) {
+        break;
+      }
+    }
+
+    sort($result);
+    $result = array_values($result);
+
+    return array($ii, $result);
+  }
+
+  private function applySetPos(array $values, array $setpos) {
+    $select = array();
+
+    $count = count($values);
+    foreach ($setpos as $pos) {
+      if ($pos > 0 && $pos <= $count) {
+        $select[] = ($pos - 1);
+      } else if ($pos < 0 && $pos >= -$count) {
+        $select[] = ($count + $pos);
+      }
+    }
+
+    sort($select);
+    return array_select_keys($values, $select);
+  }
+
+
 }
diff --git a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php
new file mode 100644
index 0000000..9e91381
--- /dev/null
+++ b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php
@@ -0,0 +1,105 @@
+<?php
+
+final class PhutilCalendarRecurrenceRuleTestCase extends PhutilTestCase {
+
+  public function testSimpleRecurrenceRules() {
+    $start = PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z');
+
+    $rrule = id(new PhutilCalendarRecurrenceRule())
+      ->setStartDateTime($start)
+      ->setFrequency('DAILY');
+
+    $set = id(new PhutilCalendarRecurrenceSet())
+      ->addSource($rrule);
+
+    $result = $set->getEventsBetween(null, null, 3);
+
+    $expect = array(
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+    );
+
+    $this->assertEqual(
+      mpull($expect, 'getISO8601'),
+      mpull($result, 'getISO8601'),
+      pht('Simple daily event.'));
+
+
+
+    $rrule = id(new PhutilCalendarRecurrenceRule())
+      ->setStartDateTime($start)
+      ->setFrequency('HOURLY')
+      ->setByHour(array(12, 13));
+
+    $set = id(new PhutilCalendarRecurrenceSet())
+      ->addSource($rrule);
+
+    $result = $set->getEventsBetween(null, null, 5);
+
+    $expect = array(
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T130000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T130000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
+    );
+
+    $this->assertEqual(
+      mpull($expect, 'getISO8601'),
+      mpull($result, 'getISO8601'),
+      pht('Hourly event with BYHOUR.'));
+
+
+    $rrule = id(new PhutilCalendarRecurrenceRule())
+      ->setStartDateTime($start)
+      ->setFrequency('YEARLY');
+
+    $set = id(new PhutilCalendarRecurrenceSet())
+      ->addSource($rrule);
+
+    $result = $set->getEventsBetween(null, null, 2);
+
+    $expect = array(
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20170101T120000Z'),
+    );
+
+    $this->assertEqual(
+      mpull($expect, 'getISO8601'),
+      mpull($result, 'getISO8601'),
+      pht('Yearly event.'));
+
+
+    // This is an efficiency test for bizarre rules: it defines a secondly
+    // event which only occurs one a year, and generates 3 instances of it.
+    // This implementation should be fast enough that this test doesn't take
+    // a significant amount of time.
+
+    $rrule = id(new PhutilCalendarRecurrenceRule())
+      ->setStartDateTime($start)
+      ->setFrequency('SECONDLY')
+      ->setByMonth(array(1))
+      ->setByMonthDay(array(1))
+      ->setByHour(array(12))
+      ->setByMinute(array(0))
+      ->setBySecond(array(0));
+
+    $set = id(new PhutilCalendarRecurrenceSet())
+      ->addSource($rrule);
+
+    $result = $set->getEventsBetween(null, null, 3);
+
+    $expect = array(
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20170101T120000Z'),
+      PhutilCalendarAbsoluteDateTime::newFromISO8601('20180101T120000Z'),
+    );
+
+    $this->assertEqual(
+      mpull($expect, 'getISO8601'),
+      mpull($result, 'getISO8601'),
+      pht('Secondly event with many constraints.'));
+  }
+
+}