diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 56f5b89f9..1c2755f71 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -36,7 +36,6 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp src/interface/widgets/dialogs/dlg_forgot_password_request.cpp src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp - src/interface/widgets/dialogs/dlg_invite_to_game.cpp src/interface/widgets/dialogs/dlg_load_deck.cpp src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp deleted file mode 100644 index b5451bb2c..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "dlg_invite_to_game.h" - -#include "../server/user/user_list_manager.h" -#include "../server/user/user_list_widget.h" -#include "../tabs/tab_supervisor.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor, - const QString &_inviteUrl, - bool _onlyBuddies, - const QStringList &_excludeUserNames, - QWidget *parent) - : QDialog(parent), tabSupervisor(_tabSupervisor), inviteUrl(_inviteUrl), onlyBuddies(_onlyBuddies), - excludeUserNames(_excludeUserNames) -{ - setModal(true); - - searchEdit = new QLineEdit(this); - searchEdit->setClearButtonEnabled(true); - connect(searchEdit, &QLineEdit::textChanged, this, &DlgInviteToGame::searchTextChanged); - - // The embedded list is the real room user list without the hover popup: - // same manager, same delegate/painter, same sections, live via manager - // signals while the modal loop runs. - UserListManager *manager = tabSupervisor->getUserListManager(); - userList = new UserListWidget(tabSupervisor, tabSupervisor->getClient(), UserListWidget::RoomList, this, - /*hasUserInfoPopup=*/false); - userList->setUserFilter([this, manager](const QString &name, bool online) { - return !excludeUserNames.contains(name) && online && !manager->isUserIgnored(name); - }); - if (onlyBuddies) { - userList->setSectioned({UserListWidget::Section::Buddy}); - } else { - userList->setSectioned({UserListWidget::Section::Buddy, UserListWidget::Section::Online}); - } - userList->bind(manager); - userList->rebuild(); - - connect(userList, &UserListWidget::userActivated, this, &DlgInviteToGame::inviteCurrentUser); - connect(userList, &UserListWidget::currentUserChanged, this, [this](const QString &userName) { - currentUserName = userName; - inviteButton->setEnabled(!userName.isEmpty()); - }); - - inviteButton = new QPushButton(this); - inviteButton->setEnabled(false); - inviteButton->setDefault(true); - connect(inviteButton, &QPushButton::clicked, this, [this] { inviteCurrentUser(currentUserName); }); - - cancelButton = new QPushButton(this); - connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); - - auto *buttonRow = new QHBoxLayout; - buttonRow->addStretch(); - buttonRow->addWidget(inviteButton); - buttonRow->addWidget(cancelButton); - - auto *layout = new QVBoxLayout(this); - layout->addWidget(searchEdit); - layout->addWidget(userList, 1); - layout->addLayout(buttonRow); - - retranslateUi(); - - // Default to a comfortably tall dialog so the list has room to breathe, - // capped by the available screen. No minimum is enforced: small screens - // and manual resizing can go shorter than this. - const QRect availableScreen = QGuiApplication::primaryScreen()->availableGeometry(); - resize(sizeHint().width(), qMin(sizeHint().height() * 3, availableScreen.height() * 4 / 5)); -} - -void DlgInviteToGame::searchTextChanged(const QString &text) -{ - userList->setFilterText(text); -} - -void DlgInviteToGame::inviteCurrentUser(const QString &userName) -{ - if (userName.isEmpty()) { - return; - } - // The invite link carries the game's id and, when the game has one, its - // description (makeGameJoinLink embeds both). Read them back so the prefix - // names the game by description first, then its id — identical to the - // context-menu invite so recipients see one consistent message style. - const QUrl inviteUrlObj(inviteUrl); - const QUrlQuery inviteQuery(inviteUrlObj); - const int gameId = inviteQuery.queryItemValue("gameid").toInt(); - const QString gameDescription = inviteQuery.queryItemValue("game"); - const QString prefix = gameDescription.isEmpty() - ? tr("Join my game (#%1):").arg(gameId) - : tr("Join my game \"%1\" (#%2):").arg(gameDescription).arg(gameId); - tabSupervisor->sendInviteToUser(userName, prefix + " " + inviteUrl); - accept(); -} - -void DlgInviteToGame::retranslateUi() -{ - setWindowTitle(tr("Invite to Game")); - searchEdit->setPlaceholderText(tr("Search users...")); - inviteButton->setText(tr("Invite")); - cancelButton->setText(tr("Cancel")); -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h b/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h deleted file mode 100644 index bbb589bdb..000000000 --- a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @file dlg_invite_to_game.h - * @ingroup RoomDialogs - */ -//! \todo Document this file. - -#ifndef DLG_INVITE_TO_GAME_H -#define DLG_INVITE_TO_GAME_H - -#include -#include - -class QLineEdit; -class QPushButton; -class TabSupervisor; -class UserListWidget; - -class DlgInviteToGame : public QDialog -{ - Q_OBJECT -public: - DlgInviteToGame(TabSupervisor *_tabSupervisor, - const QString &_inviteUrl, - bool _onlyBuddies, - const QStringList &_excludeUserNames, - QWidget *parent = nullptr); - -private slots: - void searchTextChanged(const QString &text); - void inviteCurrentUser(const QString &userName); - -private: - TabSupervisor *tabSupervisor; - QString inviteUrl; - bool onlyBuddies; - QStringList excludeUserNames; - QString currentUserName; - QLineEdit *searchEdit; - UserListWidget *userList; - QPushButton *inviteButton; - QPushButton *cancelButton; - - void retranslateUi(); -}; - -#endif diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 685325f50..b63457169 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -354,10 +354,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event, if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) { QMouseEvent *const mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::RightButton) { - // Dialog mode has no context menu: consume the press, show nothing. - if (owner->getHasUserInfoPopup()) { - owner->showContextMenu(mouseEvent->globalPosition().toPoint(), index); - } + owner->showContextMenu(mouseEvent->globalPosition().toPoint(), index); return true; } } @@ -581,10 +578,8 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, - QWidget *parent, - bool _hasUserInfoPopup) - : QGroupBox(parent), hasUserInfoPopup(_hasUserInfoPopup), tabSupervisor(_tabSupervisor), client(_client), - type(_type), onlineCount(0) + QWidget *parent) + : QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0) { avatarProvider = new UserAvatarProvider(client, this); cardArtProvider = new UserCardArtProvider(this); @@ -610,8 +605,15 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); userTree->header()->setStretchLastSection(true); - // Always create timers so callers never segfault on a null deref; - // showPopupForUser / hidePopup already guard against a null userInfoPopup. + // ── Hover popup ─────────────────────────────────────────────────────────── + userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(), + &cardArtProvider->cache(), &cardArtParamsMap, + window()); // parented to main window so it floats above siblings + + userInfoPopup->hide(); + userInfoPopup->setWindowOpacity(0.0); + userInfoPopup->installEventFilter(this); + showPopupTimer = new QTimer(this); showPopupTimer->setSingleShot(true); showPopupTimer->setInterval(280); @@ -637,104 +639,65 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, // The hover ends when the cursor leaves the user row. Empty list // space, a section divider and anything outside the tree all close // the popup, while the popup itself keeps it alive. - if (!popupPinned && userInfoPopup && !userInfoPopup->underMouse() && - (hoveredUser.isEmpty() || !userTree->underMouse())) { + if (!popupPinned && !userInfoPopup->underMouse() && (hoveredUser.isEmpty() || !userTree->underMouse())) { hidePopup(); } }); - if (hasUserInfoPopup) { - // ── Hover popup ─────────────────────────────────────────────────────── - userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(), - &cardArtProvider->cache(), &cardArtParamsMap, - window()); // parented to main window so it floats above siblings - - userInfoPopup->hide(); - userInfoPopup->setWindowOpacity(0.0); - userInfoPopup->installEventFilter(this); - - connectPopupSignals(); - } + connectPopupSignals(); userTree->setMouseTracking(true); userTree->viewport()->setMouseTracking(true); userTree->viewport()->installEventFilter(this); userTree->installEventFilter(this); // keyboard handling for section dividers - if (hasUserInfoPopup) { - // Clicking anywhere outside the list clears its selection and closes the - // popup. The filter watches all widgets because the press can land on any - // part of the window, on another list or on the popup itself. - qApp->installEventFilter(this); + // Clicking anywhere outside the list clears its selection and closes the + // popup. The filter watches all widgets because the press can land on any + // part of the window, on another list or on the popup itself. + qApp->installEventFilter(this); - // Pin on item click - connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { - // Clicking a section divider toggles it - if (sectioned && item->type() == SectionItemType) { - setExpandedProgrammatically(item, !item->isExpanded()); - handleSectionExpansion(item, item->isExpanded()); - return; - } - if (!SettingsCache::instance().appearance().getStyleUserList()) { - return; - } - if (item->type() != QTreeWidgetItem::Type) { - return; // divider rows have no user popup - } - popupPinned = false; // reset so showPopupForUser can update - showPopupForUser(static_cast(item)); - popupPinned = true; // pin after showing - }); + // Pin on item click + connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { + // Clicking a section divider toggles it + if (sectioned && item->type() == SectionItemType) { + setExpandedProgrammatically(item, !item->isExpanded()); + handleSectionExpansion(item, item->isExpanded()); + return; + } + if (!SettingsCache::instance().appearance().getStyleUserList()) { + return; + } + if (item->type() != QTreeWidgetItem::Type) { + return; // divider rows have no user popup + } + popupPinned = false; // reset so showPopupForUser can update + showPopupForUser(static_cast(item)); + popupPinned = true; // pin after showing + }); - connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, - [this](const QItemSelection &sel, const QItemSelection &) { - if (sel.isEmpty() && popupPinned) { - popupPinned = false; - hidePopup(); - } - }); + connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this](const QItemSelection &sel, const QItemSelection &) { + if (sel.isEmpty() && popupPinned) { + popupPinned = false; + hidePopup(); + } + }); - // Keyboard selection: the popup is a mouse surface, so keyboard - // navigation shows no floating popup. A pinned (clicked) popup still - // follows the selection so it does not strand on a stale user while - // arrows move the cursor. - connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) { - if (!popupPinned) { - return; // keyboard navigation shows no popup - } - if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) { - return; - } - if (current && current->type() == QTreeWidgetItem::Type) { - showPopupForUser(static_cast(current)); - } else { - popupPinned = false; - hidePopup(); - } - }); - - // Hide popup when list scrolls (reference row has moved) - connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { - showPopupTimer->stop(); - hidePopup(true); - requestAvatarsForVisibleItems(); - }); - - // Forward join requests from popup upward - connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); - } else { - // Dialog mode: keyboard selection drives the Invite button. - connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) { - const QString userName = (current && current->type() == QTreeWidgetItem::Type) - ? current->data(2, Qt::UserRole).toString() - : QString(); - emit currentUserChanged(userName); - }); - - // Keep the popup-less scroll path alive for avatar prefetch. - connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, - [this] { requestAvatarsForVisibleItems(); }); - } + // Keyboard selection: show the popup for the current row and hide it when + // the focus moves to a section divider or leaves the list entirely. The + // popup therefore follows arrow key navigation exactly like mouse hover. + // When it was pinned by a click it stays open and follows the selection. + connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) { + if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) { + return; + } + if (current && current->type() == QTreeWidgetItem::Type) { + showPopupForUser(static_cast(current)); + } else { + popupPinned = false; + hidePopup(); + } + }); // Section dividers can be collapsed/expanded by the user. Surface those // changes only from real user interaction. Programmatic expansion is @@ -744,6 +707,16 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, connect(userTree, &QTreeWidget::itemCollapsed, this, [this](QTreeWidgetItem *item) { handleSectionExpansion(item, false); }); + // Hide popup when list scrolls (reference row has moved) + connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { + showPopupTimer->stop(); + hidePopup(true); + requestAvatarsForVisibleItems(); + }); + + // Forward join requests from popup upward + connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested); + connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, &UserListWidget::refreshVisibleUserHeader); connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, &UserListWidget::refreshVisibleUserHeader); @@ -866,9 +839,6 @@ void UserListWidget::bind(UserListManager *mgr) void UserListWidget::refreshVisibleUserHeader(const QString &name) { userTree->viewport()->update(); - if (!userInfoPopup) { - return; - } if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == name) { userInfoPopup->refreshHeader(); } @@ -876,9 +846,6 @@ void UserListWidget::refreshVisibleUserHeader(const QString &name) void UserListWidget::refreshPopupButtons(const QString &userName) { - if (!userInfoPopup) { - return; - } UserListTWI *item = users.value(userName); if (!item) { return; @@ -896,9 +863,6 @@ void UserListWidget::refreshPopupButtons(const QString &userName) void UserListWidget::hideEvent(QHideEvent *e) { QGroupBox::hideEvent(e); - if (!userInfoPopup) { - return; - } showPopupTimer->stop(); hidePopupTimer->stop(); hidePopup(true); @@ -907,9 +871,6 @@ void UserListWidget::hideEvent(QHideEvent *e) void UserListWidget::showEvent(QShowEvent *e) { QGroupBox::showEvent(e); - if (!userInfoPopup) { - return; - } requestAvatarsForVisibleItems(); } @@ -982,24 +943,6 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event) } } - // Keyboard entry to the user context menu: the Menu key (or Shift+F10) - // pops the same menu the right-click shows, anchored to the focused row. - // Divider rows have no menu. Mouse-triggered context events are NOT handled - // here — the delegate's right-press path already pops the menu, and - // handling both would open two menus on one right-click. - if (hasUserInfoPopup && (obj == userTree || obj == userTree->viewport()) && event->type() == QEvent::ContextMenu) { - auto *contextEvent = static_cast(event); - if (contextEvent->reason() == QContextMenuEvent::Keyboard) { - QTreeWidgetItem *current = userTree->currentItem(); - if (current && current->type() == QTreeWidgetItem::Type) { - const QPoint globalPos = userTree->viewport()->mapToGlobal(userTree->visualItemRect(current).center()); - showContextMenu(globalPos, userTree->indexFromItem(current)); - return true; - } - return false; // divider rows: no menu - } - } - // Keyboard navigation of the section dividers. // The dividers are selectable so arrow keys land on them. When one is the // current item, Enter/Space toggle it (like a button) and Left/Right follow @@ -1021,7 +964,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event) } } - if (hasUserInfoPopup && obj == userTree->viewport()) { + if (obj == userTree->viewport()) { if (event->type() == QEvent::MouseMove) { if (!SettingsCache::instance().appearance().getStyleUserList()) { return QGroupBox::eventFilter(obj, event); @@ -1061,9 +1004,6 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event) void UserListWidget::showPopupForUser(UserListTWI *item) { - if (!userInfoPopup) { - return; - } if (!item) { return; } @@ -1122,9 +1062,6 @@ void UserListWidget::showPopupForUser(UserListTWI *item) void UserListWidget::positionPopup(UserListTWI *item) { - if (!userInfoPopup) { - return; - } if (!item) { return; } @@ -1179,9 +1116,6 @@ void UserListWidget::positionPopup(UserListTWI *item) void UserListWidget::hidePopup(bool immediate) { - if (!userInfoPopup) { - return; - } showPopupTimer->stop(); hidePopupTimer->stop(); if (!userInfoPopup->isVisible()) { @@ -1542,10 +1476,8 @@ void UserListWidget::applyFilter() int visible = 0; for (int i = 0; i < divider->childCount(); ++i) { auto *child = static_cast(divider->child(i)); - const QString name = QString::fromStdString(child->getUserInfo().name()); - const bool passesFilter = - !userFilter || userFilter(name, child->data(0, UserListRoles::Online).toBool()); - const bool match = passesFilter && (!searching || name.toLower().contains(lower)); + const bool match = + !searching || QString::fromStdString(child->getUserInfo().name()).toLower().contains(lower); child->setHidden(!match); if (match) { ++visible; @@ -1565,7 +1497,6 @@ void UserListWidget::applyFilter() } requestAvatarsForVisibleItems(); userTree->viewport()->update(); - emit userListChanged(); return; } @@ -1583,7 +1514,6 @@ void UserListWidget::applyFilter() requestAvatarsForVisibleItems(); userTree->viewport()->update(); - emit userListChanged(); } void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/) @@ -1591,38 +1521,7 @@ void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/) if (item->type() != QTreeWidgetItem::Type) { return; // divider rows open no chat } - const QString userName = item->data(2, Qt::UserRole).toString(); - if (hasUserInfoPopup) { - emit openMessageDialog(userName, true); - } else { - emit userActivated(userName); - } -} - -int UserListWidget::visibleUserRowCount() const -{ - int count = 0; - if (sectioned) { - for (const Section section : sectionIds) { - QTreeWidgetItem *divider = sectionItems.value(section); - if (!divider || divider->isHidden()) { - continue; - } - for (int i = 0; i < divider->childCount(); ++i) { - if (!divider->child(i)->isHidden()) { - ++count; - } - } - } - return count; - } - for (int i = 0; i < userTree->topLevelItemCount(); ++i) { - QTreeWidgetItem *item = userTree->topLevelItem(i); - if (!item->isHidden() && item->type() == QTreeWidgetItem::Type) { - ++count; - } - } - return count; + emit openMessageDialog(item->data(2, Qt::UserRole).toString(), true); } void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index) @@ -1871,13 +1770,6 @@ UserListTWI *UserListWidget::ensureSectionMembership(Section section, const Serv updateCardArtParams(user, userName); - // Dialog mode: rows that fail the user filter never exist. applyFilter() - // re-checks the predicate on every pass so a live state change (e.g. the - // user being ignored mid-dialog) hides an already created row. - if (userFilter && !userFilter(userName, online)) { - return nullptr; - } - QTreeWidgetItem *divider = sectionItems.value(section); if (!divider) { return nullptr; diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index 0407ad8ca..d97843264 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -173,8 +173,6 @@ private: QString hoveredUser; bool popupPinned = false; bool bulkLoading = false; - bool hasUserInfoPopup = true; - std::function userFilter; /** * Popup functions are anchored on the row, not the user name. In sectioned @@ -244,19 +242,12 @@ signals: void removeIgnore(const QString &userName); void joinGameRequested(int gameId, int roomId, bool asSpectator); void sectionExpanded(Section section, bool expanded); - /** Dialog mode: the user activated (Enter/double-click) the given row. */ - void userActivated(const QString &userName); - /** Dialog mode: the current row changed; empty string means no user row. */ - void currentUserChanged(const QString &userName); - /** The set of visible rows changed (filter, search or a live mutation). */ - void userListChanged(); public: UserListWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, - QWidget *parent = nullptr, - bool hasUserInfoPopup = true); + QWidget *parent = nullptr); ~UserListWidget() override; void bind(UserListManager *mgr); void applyDisplayMode(); @@ -272,16 +263,6 @@ public: void setShowTitle(bool showTitle); void setSectioned(const QList
&ids); void setSectionExpanded(Section section, bool expanded); - /** Dialog mode: rows that fail the predicate are never shown. */ - void setUserFilter(std::function filter) - { - userFilter = std::move(filter); - } - [[nodiscard]] int visibleUserRowCount() const; - [[nodiscard]] bool getHasUserInfoPopup() const - { - return hasUserInfoPopup; - } [[nodiscard]] const QList
&getSectionIds() const { return sectionIds; diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index dbf4a5a4a..513b7c926 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -20,7 +20,6 @@ #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/dialogs/dlg_create_game.h" -#include "../interface/widgets/dialogs/dlg_invite_to_game.h" #include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/utility/completer_utils.h" @@ -44,12 +43,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -299,9 +296,6 @@ void TabGame::retranslateUi() QString tabText = " | " + type + " #" + QString::number(game->getGameMetaInfo()->gameId()); updatePlayerListDockTitle(); - if (inviteButton) { - inviteButton->setText(tr("Invite")); - } cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString())); messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString())); if (replayDock) { @@ -343,9 +337,6 @@ void TabGame::retranslateUi() if (aCopyGameLink) { aCopyGameLink->setText(tr("Cop&y game link")); } - if (aInviteToGame) { - aInviteToGame->setText(tr("Invite to Game...")); - } if (aConcede) { if (game->getPlayerManager()->isMainPlayerConceded()) { aConcede->setText(tr("Un&concede")); @@ -522,47 +513,6 @@ void TabGame::actCopyGameLink() QApplication::clipboard()->setText(link); } -void TabGame::updateInviteButtonState() -{ - // The dock button stays conservative (pre-start, not full); the menu action - // additionally covers started/full games, which are legitimate spectate - // invites, so it only needs the server-linked + not-closed conditions. - const bool canInvite = !tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed() && - !game->getGameMetaInfo()->started() && - game->getPlayerManager()->getPlayerCount() < game->getGameMetaInfo()->maxPlayers(); - if (inviteButton) { - inviteButton->setVisible(canInvite); - } - if (aInviteToGame) { - aInviteToGame->setEnabled(!tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed()); - } -} - -void TabGame::actInviteToGame() -{ - if (!tabSupervisor || tabSupervisor->getIsLocalGame()) { - return; - } - - GameMetaInfo *metaInfo = game->getGameMetaInfo(); - const QString inviteUrl = makeGameJoinLink( - tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(), metaInfo->proto().room_id(), - metaInfo->gameId(), QString::fromStdString(metaInfo->proto().description())); - - QStringList excludeUserNames; - excludeUserNames << tabSupervisor->getUserListManager()->getOwnUsername(); - for (auto player : game->getPlayerManager()->getPlayers()) { - excludeUserNames << player->getPlayerInfo()->getName(); - } - for (auto it = game->getPlayerManager()->getSpectators().cbegin(); - it != game->getPlayerManager()->getSpectators().cend(); ++it) { - excludeUserNames << QString::fromStdString(it.value().name()); - } - - DlgInviteToGame dlg(tabSupervisor, inviteUrl, metaInfo->proto().only_buddies(), excludeUserNames, this); - dlg.exec(); -} - void TabGame::actConcede() { PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); @@ -1054,8 +1004,6 @@ void TabGame::createMenuItems() aCopyGameLink = new QAction(this); aCopyGameLink->setEnabled(!tabSupervisor->getIsLocalGame() && !tabSupervisor->getClient()->serverName().isEmpty()); connect(aCopyGameLink, &QAction::triggered, this, &TabGame::actCopyGameLink); - aInviteToGame = new QAction(this); - connect(aInviteToGame, &QAction::triggered, this, &TabGame::actInviteToGame); aConcede = new QAction(this); connect(aConcede, &QAction::triggered, this, &TabGame::actConcede); if (!game->getGameMetaInfo()->started()) { @@ -1095,7 +1043,6 @@ void TabGame::createMenuItems() gameMenu->addSeparator(); gameMenu->addAction(aGameInfo); gameMenu->addAction(aCopyGameLink); - gameMenu->addAction(aInviteToGame); gameMenu->addAction(aConcede); gameMenu->addAction(aFocusChat); gameMenu->addAction(aLeaveGame); @@ -1104,9 +1051,6 @@ void TabGame::createMenuItems() aCardMenu = gameMenu->addMenu(new QMenu(this)); - // Sync the new action with the same state the dock button already shows. - updateInviteButtonState(); - addTabMenu(gameMenu); } @@ -1122,7 +1066,6 @@ void TabGame::createReplayMenuItems() aResetLayout = nullptr; aGameInfo = nullptr; aCopyGameLink = nullptr; - aInviteToGame = nullptr; aConcede = nullptr; aFocusChat = nullptr; aLeaveGame = new QAction(this); @@ -1321,29 +1264,11 @@ void TabGame::createPlayerListDock(bool bReplay) } playerListWidget->setFocusPolicy(Qt::NoFocus); - auto *playerListBox = new QWidget(this); - auto *vbox = new QVBoxLayout(playerListBox); - vbox->setContentsMargins(0, 0, 0, 0); - vbox->setSpacing(0); - - vbox->addWidget(playerListWidget); - - if (!bReplay) { - inviteButton = new QPushButton(tr("Invite"), playerListBox); - inviteButton->setVisible(false); - connect(inviteButton, &QPushButton::clicked, this, &TabGame::actInviteToGame); - vbox->addWidget(inviteButton); - - connect(game->getGameMetaInfo(), &GameMetaInfo::startedChanged, this, &TabGame::updateInviteButtonState); - connect(game->getPlayerManager(), &PlayerManager::playerCountChanged, this, &TabGame::updateInviteButtonState); - updateInviteButtonState(); - } - playerListDock = new QDockWidget(this); playerListDock->setObjectName("playerListDock"); playerListDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable); - playerListDock->setWidget(playerListBox); + playerListDock->setWidget(playerListWidget); playerListDock->setFloating(false); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index a05b49a9f..b6555deef 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -37,7 +37,6 @@ class CardInfoFrameWidget; class QTimer; class QSplitter; class QLabel; -class QPushButton; class QToolButton; class QMenu; class ZoneViewLayout; @@ -70,7 +69,6 @@ private: CardInfoFrameWidget *cardInfoFrameWidget; PlayerListWidget *playerListWidget; - QPushButton *inviteButton = nullptr; QLabel *timeElapsedLabel; MessageLogWidget *messageLog; QLabel *sayLabel; @@ -88,7 +86,6 @@ private: QAction *aGameInfo, *aConcede, *aCopyGameLink, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn, *aReverseTurn, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout; QAction *aFocusChat; - QAction *aInviteToGame = nullptr; QList phaseActions; QAction *aCardMenu; @@ -131,7 +128,6 @@ private: void createPlayAreaWidget(bool bReplay = false); void createDeckViewContainerWidget(bool bReplay = false); void createReplayDock(GameReplay *replay); - void updateInviteButtonState(); signals: void gameClosing(TabGame *tab); void containerProcessingStarted(const GameEventContext &context); @@ -151,7 +147,6 @@ private slots: void setCardMenu(CardMenu *menu); void actGameInfo(); - void actInviteToGame(); void actConcede(); void actCopyGameLink(); void actRemoveLocalArrows(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.cpp b/cockatrice/src/interface/widgets/tabs/tab_message.cpp index 9506d96f3..d482d3dd7 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_message.cpp @@ -128,12 +128,6 @@ bool TabMessage::isUserOnline() const return userOnline; } -void TabMessage::sendInviteMessage(const QString &text) -{ - sayEdit->setText(text); - sendMessage(); -} - void TabMessage::messageSent(const Response &response, const CommandContainer & /*commandContainer*/, const QVariant &extraData) diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.h b/cockatrice/src/interface/widgets/tabs/tab_message.h index 0e6d66d4c..e9b987ce2 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.h +++ b/cockatrice/src/interface/widgets/tabs/tab_message.h @@ -66,7 +66,6 @@ public: [[nodiscard]] bool isUserOnline() const; void sendPrivateMessage(const QString &text); - void sendInviteMessage(const QString &text); private: bool shouldShowSystemPopup(const Event_UserMessage &event); diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index 016d96434..77b93802a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -993,8 +993,8 @@ QList TabSupervisor::getGameInviteLinksForRoom(int roomId) con void TabSupervisor::sendInviteToUser(const QString &userName, const QString &inviteText) { TabMessage *tab = addMessageTab(userName, true); - if (tab) { - tab->sendInviteMessage(inviteText); + if (tab && tab->isUserOnline()) { + tab->sendPrivateMessage(inviteText); } } diff --git a/libcockatrice_utility/libcockatrice/utility/peglib.h b/libcockatrice_utility/libcockatrice/utility/peglib.h index a67a86549..e7e558dff 100644 --- a/libcockatrice_utility/libcockatrice/utility/peglib.h +++ b/libcockatrice_utility/libcockatrice/utility/peglib.h @@ -1,4 +1,4 @@ -// +// // peglib.h // // Copyright (c) 2022 Yuji Hirose. All rights reserved. @@ -7,9 +7,6 @@ #pragma once -#define CPPPEGLIB_VERSION "1.16.0" -#define CPPPEGLIB_VERSION_NUM "0x011000" - /* * Configuration */ @@ -49,8 +46,6 @@ namespace peg { -struct GrammarBlob; - /*----------------------------------------------------------------------------- * scope_exit *---------------------------------------------------------------------------*/ @@ -340,14 +335,6 @@ inline std::string resolve_escape_sequence(const char *s, size_t n) { r += ']'; i++; break; - case '^': - r += '^'; - i++; - break; - case '-': - r += '-'; - i++; - break; case '\\': r += '\\'; i++; @@ -374,46 +361,6 @@ inline std::string resolve_escape_sequence(const char *s, size_t n) { return r; } -/* - * Predefined character classes (ASCII semantics) - */ -inline const std::vector> * -predefined_character_class(std::string_view name) { - static const std::map>> - table = { - {"alnum", {{'0', '9'}, {'A', 'Z'}, {'a', 'z'}}}, - {"alpha", {{'A', 'Z'}, {'a', 'z'}}}, - {"ascii", {{0x00, 0x7F}}}, - {"blank", {{'\t', '\t'}, {' ', ' '}}}, - {"cntrl", {{0x00, 0x1F}, {0x7F, 0x7F}}}, - {"digit", {{'0', '9'}}}, - {"graph", {{0x21, 0x7E}}}, - {"lower", {{'a', 'z'}}}, - {"print", {{0x20, 0x7E}}}, - {"punct", {{0x21, 0x2F}, {0x3A, 0x40}, {0x5B, 0x60}, {0x7B, 0x7E}}}, - {"space", {{'\t', '\r'}, {' ', ' '}}}, - {"upper", {{'A', 'Z'}}}, - {"word", {{'0', '9'}, {'A', 'Z'}, {'_', '_'}, {'a', 'z'}}}, - {"xdigit", {{'0', '9'}, {'A', 'F'}, {'a', 'f'}}}, - }; - auto it = table.find(name); - return it != table.end() ? &it->second : nullptr; -} - -// Ranges must be sorted and non-overlapping. -inline std::vector> complement_character_ranges( - const std::vector> &ranges) { - std::vector> r; - char32_t next = 0; - for (const auto &[lo, hi] : ranges) { - if (lo > next) { r.emplace_back(next, lo - 1); } - next = hi + 1; - } - if (next <= 0x10FFFF) { r.emplace_back(next, 0x10FFFF); } - return r; -} - /*----------------------------------------------------------------------------- * token_to_number_ - This function should be removed eventually *---------------------------------------------------------------------------*/ @@ -501,7 +448,6 @@ public: size_t items_count() const { return items_count_; } friend struct ComputeFirstSet; - friend struct GrammarBlob; private: struct Info { @@ -560,7 +506,7 @@ inline constexpr unsigned int str2tag(std::string_view sv) { namespace udl { -inline constexpr unsigned int operator""_(const char *s, size_t l) { +inline constexpr unsigned int operator"" _(const char *s, size_t l) { return str2tag_core(s, l, 0); } @@ -778,25 +724,6 @@ inline bool fail(size_t len) { return len == static_cast(-1); } using Log = std::function; -/* - * ErrorReport - structured error information passed to an ErrorReporter. - * Unlike Log, nothing is flattened into a display string, so applications - * can map errors to their own error types, localize messages, or feed - * diagnostics to IDEs. - */ -struct ErrorReport { - size_t line = 0; // 1-based - size_t col = 1; // 1-based - size_t position = 0; // byte offset in the input - std::string unexpected_token; // heuristic token at the error position - std::vector expected_literals; - std::vector expected_rules; // rules starting with '_' excluded - std::string message; // custom error_message if any (placeholders resolved) - std::string label; // rule name or recovery label the error belongs to -}; - -using ErrorReporter = std::function; - /* * ErrorInfo */ @@ -825,11 +752,7 @@ struct ErrorInfo { expected_tokens.emplace_back(error_literal, error_rule); } - void output_log(const Log &log, const char *s, size_t n) { - output_log(log, nullptr, s, n); - } - void output_log(const Log &log, const ErrorReporter &reporter, const char *s, - size_t n); + void output_log(const Log &log, const char *s, size_t n); private: int cast_char(char c) const { return static_cast(c); } @@ -885,131 +808,6 @@ using TracerLeave = std::function; -// Packrat memoization table: open-addressing hash map keyed by the fused -// (position * rule count + rule id) index. The insert-heavy access pattern -// makes node-based containers a bottleneck, so keys and lengths live in one -// flat array of 16-byte POD slots probed linearly; semantic values go into a -// parallel array that is never allocated when no cached result carries a -// value. Erased slots become tombstones (erase only happens during -// left-recursion cache invalidation). -class PackratCache { -public: - explicit PackratCache(size_t expected_entries) { - while (initial_capacity_ < expected_entries) { - initial_capacity_ *= 2; - } - } - - bool find(size_t key, size_t &len, std::any &val) const { - if (slots_.empty()) { return false; } - auto mask = slots_.size() - 1; - auto i = mix(key) & mask; - while (true) { - auto &slot = slots_[i]; - if (slot.key == key) { - len = slot.len; - if (!vals_.empty()) { - val = vals_[i]; - } else { - val.reset(); - } - return true; - } - if (slot.key == kEmpty) { return false; } - i = (i + 1) & mask; - } - } - - void insert_or_assign(size_t key, size_t len, const std::any &val) { - if (slots_.empty() || (used_ + 1) * 4 > slots_.size() * 3) { grow(); } - auto mask = slots_.size() - 1; - auto i = mix(key) & mask; - auto insert_pos = kEmpty; - while (true) { - auto &slot = slots_[i]; - if (slot.key == key) { - insert_pos = i; - break; - } - if (slot.key == kTombstone) { - if (insert_pos == kEmpty) { insert_pos = i; } - } else if (slot.key == kEmpty) { - if (insert_pos == kEmpty) { insert_pos = i; } - if (slots_[insert_pos].key == kEmpty) { used_++; } - break; - } - i = (i + 1) & mask; - } - auto &dest = slots_[insert_pos]; - dest.key = key; - dest.len = len; - if (val.has_value()) { - if (vals_.empty()) { vals_.resize(slots_.size()); } - vals_[insert_pos] = val; - } else if (!vals_.empty()) { - vals_[insert_pos].reset(); - } - } - - void erase(size_t key) { - if (slots_.empty()) { return; } - auto mask = slots_.size() - 1; - auto i = mix(key) & mask; - while (true) { - auto &slot = slots_[i]; - if (slot.key == key) { - slot.key = kTombstone; - if (!vals_.empty()) { vals_[i].reset(); } - return; - } - if (slot.key == kEmpty) { return; } - i = (i + 1) & mask; - } - } - -private: - static constexpr size_t kEmpty = static_cast(-1); - static constexpr size_t kTombstone = static_cast(-2); - - struct Slot { - size_t key = kEmpty; - size_t len = 0; - }; - - static size_t mix(size_t key) { - // Mix in 64 bits so `h >> 32` stays well-defined where size_t is 32-bit - // (wasm32); on 64-bit targets this is bit-identical to the size_t mix. - auto h = static_cast(key) * 0x9E3779B97F4A7C15ull; - return static_cast(h ^ (h >> 32)); - } - - void grow() { - auto new_cap = slots_.empty() ? initial_capacity_ : slots_.size() * 2; - std::vector old_slots = std::move(slots_); - std::vector old_vals = std::move(vals_); - slots_.assign(new_cap, Slot{}); - if (!old_vals.empty()) { vals_.assign(new_cap, std::any()); } - used_ = 0; - auto mask = new_cap - 1; - for (size_t j = 0; j < old_slots.size(); j++) { - auto &slot = old_slots[j]; - if (slot.key == kEmpty || slot.key == kTombstone) { continue; } - auto i = mix(slot.key) & mask; - while (slots_[i].key != kEmpty) { - i = (i + 1) & mask; - } - slots_[i] = slot; - if (!old_vals.empty()) { vals_[i] = std::move(old_vals[j]); } - used_++; - } - } - - size_t initial_capacity_ = 1024; - std::vector slots_; - std::vector vals_; - size_t used_ = 0; // occupied + tombstone slots -}; - class Context { public: const char *path; @@ -1019,18 +817,11 @@ public: ErrorInfo error_info; bool recovered = false; - std::vector> value_stack; + std::vector> value_stack; size_t value_stack_size = 0; std::vector rule_stack; - - // One frame per rule reference: the macro arguments in scope, and the - // instantiation they identify (0 for anything but a left-recursive macro). - struct ArgsFrame { - std::vector> args; - size_t macro_inst = 0; - }; - std::vector args_stack; + std::vector>> args_stack; size_t in_token_boundary_count = 0; @@ -1045,115 +836,68 @@ public: const size_t def_count; const bool enablePackratParsing; - const std::vector *packrat_index; // def_id -> cache slot or -1 - size_t packrat_cached_count; // number of memoized rules std::vector cache_registered; std::vector cache_success; - // Innermost active start position per rule; re-entry guard for rules that - // are not memoized (replaces the per-position bitvector for them). - std::vector active_pos; - PackratCache cache_values; + std::map, std::tuple> + cache_values; // Left recursion support struct LRMemo { size_t len = static_cast(-1); std::any val; }; - - // A left-recursive rule instance: the definition plus, for a macro, the - // instantiation it was invoked with (0 for a plain rule). Two - // instantiations of the same macro grow independent seeds. - using LRRule = std::pair; - using LRKey = std::pair; - - std::map lr_memo; + std::map, LRMemo> lr_memo; // Rules whose lr_memo was hit during the current parse scope. // Used to track LR cycle membership. - std::set lr_refs_hit; + std::set lr_refs_hit; // Rules currently in their seeding/growing phase at a given position. // Protected from having their lr_memo erased by inner growers. - std::set lr_active_seeds; - - // Interned macro instantiations: (definition, resolved arguments) -> id. - std::map, size_t> macro_inst_ids; - size_t next_macro_inst_ = 1; - - // Map a def_id to its slot in the cache tables, or -1 for guard-only - // rules (not memoized). - int32_t cache_slot(size_t def_id) const { - if (!packrat_index) { return static_cast(def_id); } - return def_id < packrat_index->size() ? (*packrat_index)[def_id] : -1; - } + std::set> lr_active_seeds; void clear_packrat_cache(const char *pos, size_t def_id) { if (!enablePackratParsing) { return; } - auto slot = cache_slot(def_id); - if (slot < 0) { return; } auto col = static_cast(pos - s); - auto idx = packrat_cached_count * col + static_cast(slot); + auto idx = def_count * col + def_id; if (idx < cache_registered.size()) { cache_registered[idx] = false; cache_success[idx] = false; } - cache_values.erase(idx); + cache_values.erase(std::make_pair(col, def_id)); } void write_packrat_cache(const char *pos, size_t def_id, size_t len, const std::any &val) { if (!enablePackratParsing) { return; } - auto slot = cache_slot(def_id); - if (slot < 0) { return; } auto col = pos - s; - auto idx = packrat_cached_count * static_cast(col) + - static_cast(slot); + auto idx = def_count * static_cast(col) + def_id; if (idx >= cache_registered.size()) { return; } cache_registered[idx] = true; cache_success[idx] = true; - cache_values.insert_or_assign(idx, len, val); + auto key = std::pair(col, def_id); + cache_values[key] = std::pair(len, val); } TracerEnter tracer_enter; TracerLeave tracer_leave; - const bool has_tracer; std::any trace_data; const bool verbose_trace; - // Byte-wise tolower frozen at parse start, so case-insensitive matching - // avoids a locale-sensitive libc call per input byte. - unsigned char tolower_table[256]; - Log log; - ErrorReporter error_reporter; Context(const char *path, const char *s, size_t l, size_t def_count, std::shared_ptr whitespaceOpe, std::shared_ptr wordOpe, bool enablePackratParsing, TracerEnter tracer_enter, TracerLeave tracer_leave, std::any trace_data, bool verbose_trace, - Log log, ErrorReporter error_reporter = nullptr, - const std::vector *packrat_index = nullptr, - size_t packrat_cached_count = 0) + Log log) : path(path), s(s), l(l), whitespaceOpe(whitespaceOpe), wordOpe(wordOpe), def_count(def_count), enablePackratParsing(enablePackratParsing), - packrat_index(packrat_index), - packrat_cached_count(packrat_index ? packrat_cached_count : def_count), - cache_registered( - enablePackratParsing ? this->packrat_cached_count * (l + 1) : 0), - cache_success( - enablePackratParsing ? this->packrat_cached_count * (l + 1) : 0), - active_pos(enablePackratParsing ? def_count : 0, nullptr), - cache_values(enablePackratParsing ? (packrat_index ? l / 8 + 16 : l / 2) - : 0), + cache_registered(enablePackratParsing ? def_count * (l + 1) : 0), + cache_success(enablePackratParsing ? def_count * (l + 1) : 0), tracer_enter(tracer_enter), tracer_leave(tracer_leave), - has_tracer(tracer_enter && tracer_leave), trace_data(trace_data), - verbose_trace(verbose_trace), log(log), error_reporter(error_reporter) { - - for (size_t i = 0; i < 256; i++) { - tolower_table[i] = - static_cast(std::tolower(static_cast(i))); - } + trace_data(trace_data), verbose_trace(verbose_trace), log(log) { push_args({}); } @@ -1174,6 +918,11 @@ public: }; std::vector *packrat_stats = nullptr; + // Per-rule packrat filter: if set, only rules with filter[def_id]=true + // use full memoization (cache_values map). Others use bitvector-only + // re-entry guard. + const std::vector *packrat_rule_filter = nullptr; + template void packrat(const char *a_s, size_t def_id, size_t &len, std::any &val, T fn) { @@ -1182,47 +931,23 @@ public: return; } - auto slot = cache_slot(def_id); - if (slot < 0) { - // Guard-only rule: no memoization. Recursion at the same position is - // caught by the per-rule active-position guard. - if (active_pos[def_id] == a_s) { - if (packrat_stats && def_id < packrat_stats->size()) { - (*packrat_stats)[def_id].hits++; - } - len = static_cast(-1); - return; - } - if (packrat_stats && def_id < packrat_stats->size()) { - (*packrat_stats)[def_id].misses++; - } - auto save = active_pos[def_id]; - active_pos[def_id] = a_s; - fn(val); - active_pos[def_id] = save; - return; - } - auto col = a_s - s; - auto idx = packrat_cached_count * static_cast(col) + - static_cast(slot); + auto idx = def_count * static_cast(col) + def_id; if (cache_registered[idx]) { if (packrat_stats && def_id < packrat_stats->size()) { (*packrat_stats)[def_id].hits++; } if (cache_success[idx]) { - if (!cache_values.find(idx, len, val)) { - len = 0; - val.reset(); - } + auto key = std::pair(col, def_id); + std::tie(len, val) = cache_values[key]; return; } else { len = static_cast(-1); return; } } else { - // Pre-register as failure (re-entry guard + failure memoization) + // Pre-register as failure (re-entry guard for all rules) cache_registered[idx] = true; cache_success[idx] = false; @@ -1232,7 +957,15 @@ public: fn(val); - if (success(len)) { write_packrat_cache(a_s, def_id, len, val); } + bool full_memo = + !packrat_rule_filter || (def_id < packrat_rule_filter->size() && + (*packrat_rule_filter)[def_id]); + if (full_memo) { + if (success(len)) { write_packrat_cache(a_s, def_id, len, val); } + } else { + // Guard-only: undo registration so future calls re-parse + cache_registered[idx] = false; + } return; } } @@ -1241,7 +974,7 @@ public: SemanticValues &push_semantic_values_scope() { assert(value_stack_size <= value_stack.size()); if (value_stack_size == value_stack.size()) { - value_stack.emplace_back(std::make_unique(this)); + value_stack.emplace_back(std::make_shared(this)); } else { auto &vs = *value_stack[value_stack_size]; if (!vs.empty()) { @@ -1263,31 +996,14 @@ public: void pop_semantic_values_scope() { value_stack_size--; } // Arguments - void push_args(std::vector> &&args, - size_t macro_inst = 0) { - args_stack.push_back({std::move(args), macro_inst}); + void push_args(std::vector> &&args) { + args_stack.emplace_back(std::move(args)); } void pop_args() { args_stack.pop_back(); } const std::vector> &top_args() const { - return args_stack[args_stack.size() - 1].args; - } - - size_t top_macro_inst() const { - return args_stack[args_stack.size() - 1].macro_inst; - } - - // Identify a macro invocation by what its resolved arguments denote (see - // macro_inst_key). `Sum(A)` inside `Sum(N)`'s own body resolves A back to - // the argument the outer call was given, so both invocations intern to the - // same id and the inner one finds the outer's seed — which is what makes - // growing terminate. - size_t intern_macro_inst(std::vector &&key) { - auto [it, inserted] = - macro_inst_ids.emplace(std::move(key), next_macro_inst_); - if (inserted) { next_macro_inst_++; } - return it->second; + return args_stack[args_stack.size() - 1]; } // Snapshot/Rollback @@ -1453,8 +1169,8 @@ private: lower_heap.reset(new char[id_len]); lower = lower_heap.get(); } - std::transform(s, s + id_len, lower, [&c](unsigned char ch) { - return static_cast(c.tolower_table[ch]); + std::transform(s, s + id_len, lower, [](unsigned char ch) { + return static_cast(std::tolower(ch)); }); std::string_view lower_sv(lower, id_len); @@ -1525,8 +1241,7 @@ public: const auto &fs = first_sets_[id]; if (!fs.any_char && !fs.can_be_empty && !fs.chars.test(static_cast(*s))) { - if ((c.log || c.error_reporter) && - (fs.first_literal || fs.first_rule)) { + if (c.log && (fs.first_literal || fs.first_rule)) { if (c.error_info.error_pos <= s) { if (c.error_info.error_pos < s || !(id > 0)) { c.error_info.error_pos = s; @@ -1802,8 +1517,6 @@ public: void accept(Visitor &v) override; friend struct ComputeFirstSet; - friend struct GrammarBlob; - friend struct OpeSignature; bool is_ascii_only() const { return is_ascii_only_; } const std::bitset<256> &ascii_bitset() const { return ascii_bitset_; } @@ -2086,10 +1799,6 @@ public: std::shared_ptr atom_; std::shared_ptr binop_; BinOpeInfo info_; - // Owned backing storage for info_ keys when this node is built by - // GrammarBlob::deserialize. Grammars parsed from source leave this empty and - // point info_ keys into the retained grammar text instead. - std::vector info_keys_; const Definition &rule_; private: @@ -2470,35 +2179,11 @@ struct DetectLeftRecursion : public TraversalVisitor { const char *error_s = nullptr; - // What a bare parameter reference denotes, plus the frame it was found at - // -- see visit_in_defining_scope. - struct ResolvedArg { - std::shared_ptr ope; - size_t depth = 0; - }; - ResolvedArg resolve_macro_arg(size_t iarg) const; - void visit_in_defining_scope(const ResolvedArg &arg); - - // A macro's body depends on its arguments, so "already visited" has to be - // per instantiation, not per name: in `A <- W('z') / W(A)`, visiting W with - // 'z' says nothing about W with A. Instantiations are identified by their - // resolved arguments, the same way as at parse time. - size_t intern_macro_inst(const Reference &ope); - - // A macro that instantiates itself with a growing argument - // (`M(s) <- M(s / 'x')`) has no finite set of instantiations. Stop - // descending instead of looping forever; the rule is then reported as - // non-left-recursive, which is what this analysis did for every macro - // before it became instantiation-aware. The bound is on nesting depth in - // general, not self-recursion specifically, so it also caps any other - // chain of nested macro calls -- generously, for real grammars. - static const size_t max_macro_inst_depth = 32; + std::shared_ptr resolve_macro_arg(size_t iarg) const; private: std::string name_; - std::set> refs_; - std::map, size_t> macro_inst_ids_; - size_t next_macro_inst_ = 1; + std::unordered_set refs_; bool done_ = false; std::vector> *> macro_args_stack_; }; @@ -2536,104 +2221,6 @@ struct ComputeCanBeEmpty : public TraversalVisitor { void visit(Cut &) override { result = false; } }; -// Structural signature of an Ope. Two alternatives whose first k elements -// have equal signatures consume the same text, so their (k+1)-th elements -// start at the same position. Opes whose state cannot be serialized get -// their address instead: that only ever reads as "these differ", which -// costs an optimization rather than adding one. -struct OpeSignature : public Ope::Visitor { - using Ope::Visitor::visit; - std::string s; - - void visit(Sequence &ope) override { group("seq", ope.opes_); } - void visit(PrioritizedChoice &ope) override { group("cho", ope.opes_); } - void visit(Repetition &ope) override { - s += "(rep " + std::to_string(ope.min_) + " " + - (ope.max_ == std::numeric_limits::max() - ? std::string("inf") - : std::to_string(ope.max_)); - wrap(*ope.ope_); - } - void visit(AndPredicate &ope) override { unary("and", *ope.ope_); } - void visit(NotPredicate &ope) override { unary("not", *ope.ope_); } - void visit(CaptureScope &ope) override { unary("cps", *ope.ope_); } - void visit(Capture &ope) override { unary("cap", *ope.ope_); } - void visit(TokenBoundary &ope) override { unary("tok", *ope.ope_); } - void visit(Ignore &ope) override { unary("ign", *ope.ope_); } - void visit(Whitespace &ope) override { unary("wsp", *ope.ope_); } - void visit(Recovery &ope) override { unary("rec", *ope.ope_); } - // A rule is named, never expanded — that is what keeps a recursive - // grammar's signature finite. WeakHolder only ever wraps a Holder, so - // descending through it lands on a name too. - void visit(Holder &ope) override { s += "(hld " + ope.name() + ")"; } - void visit(WeakHolder &ope) override { - if (auto p = ope.weak_.lock()) { - unary("wek", *p); - } else { - opaque(&ope); - } - } - void visit(Reference &ope) override { - s += "(ref " + ope.name_; - for (auto &arg : ope.args_) { - s += ' '; - arg->accept(*this); - } - s += ')'; - } - void visit(LiteralString &ope) override { - s += "(lit " + std::to_string(ope.ignore_case_) + " " + ope.lit_ + ")"; - } - void visit(CharacterClass &ope) override { - s += "(cls " + std::to_string(ope.negated_) + " " + - std::to_string(ope.ignore_case_); - for (const auto &[lo, hi] : ope.ranges_) { - s += " " + std::to_string(static_cast(lo)) + "-" + - std::to_string(static_cast(hi)); - } - s += ')'; - } - void visit(Character &ope) override { - s += "(chr " + std::to_string(static_cast(ope.ch_)) + ")"; - } - void visit(AnyCharacter &) override { s += "(any)"; } - void visit(Dictionary &ope) override { opaque(&ope); } - void visit(User &ope) override { opaque(&ope); } - void visit(BackReference &ope) override { opaque(&ope); } - void visit(PrecedenceClimbing &ope) override { opaque(&ope); } - void visit(Cut &ope) override { opaque(&ope); } - - static std::string get(Ope &ope) { - OpeSignature vis; - ope.accept(vis); - return std::move(vis.s); - } - -private: - void group(const char *tag, const std::vector> &v) { - s += '('; - s += tag; - for (const auto &op : v) { - s += ' '; - op->accept(*this); - } - s += ')'; - } - void unary(const char *tag, Ope &inner) { - s += '('; - s += tag; - wrap(inner); - } - void wrap(Ope &inner) { - s += ' '; - inner.accept(*this); - s += ')'; - } - void opaque(const void *p) { - s += "(opq " + std::to_string(reinterpret_cast(p)) + ")"; - } -}; - struct HasEmptyElement : public TraversalVisitor { using TraversalVisitor::visit; @@ -2984,7 +2571,6 @@ struct SetupFirstSets : public TraversalVisitor { if (cc && cc->is_ascii_only()) { ope.span_bitset_ = &cc->ascii_bitset(); } } void visit(Reference &ope) override; - void visit(Holder &ope) override; private: ComputeFirstSet::FirstSetCache first_set_cache_; @@ -3031,40 +2617,37 @@ public: } Result parse(const char *s, size_t n, const char *path = nullptr, - Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + Log log = nullptr) const { SemanticValues vs; std::any dt; - return parse_core(s, n, vs, dt, path, log, error_reporter); + return parse_core(s, n, vs, dt, path, log); } - Result parse(const char *s, const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + Result parse(const char *s, const char *path = nullptr, + Log log = nullptr) const { auto n = strlen(s); - return parse(s, n, path, log, error_reporter); + return parse(s, n, path, log); } Result parse(const char *s, size_t n, std::any &dt, - const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + const char *path = nullptr, Log log = nullptr) const { SemanticValues vs; - return parse_core(s, n, vs, dt, path, log, error_reporter); + return parse_core(s, n, vs, dt, path, log); } Result parse(const char *s, std::any &dt, const char *path = nullptr, - Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + Log log = nullptr) const { auto n = strlen(s); - return parse(s, n, dt, path, log, error_reporter); + return parse(s, n, dt, path, log); } template Result parse_and_get_value(const char *s, size_t n, T &val, - const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + const char *path = nullptr, + Log log = nullptr) const { SemanticValues vs; std::any dt; - auto r = parse_core(s, n, vs, dt, path, log, error_reporter); + auto r = parse_core(s, n, vs, dt, path, log); if (r.ret && !vs.empty() && vs.front().has_value()) { val = std::any_cast(vs[0]); } @@ -3073,18 +2656,17 @@ public: template Result parse_and_get_value(const char *s, T &val, const char *path = nullptr, - Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + Log log = nullptr) const { auto n = strlen(s); - return parse_and_get_value(s, n, val, path, log, error_reporter); + return parse_and_get_value(s, n, val, path, log); } template Result parse_and_get_value(const char *s, size_t n, std::any &dt, T &val, - const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + const char *path = nullptr, + Log log = nullptr) const { SemanticValues vs; - auto r = parse_core(s, n, vs, dt, path, log, error_reporter); + auto r = parse_core(s, n, vs, dt, path, log); if (r.ret && !vs.empty() && vs.front().has_value()) { val = std::any_cast(vs[0]); } @@ -3093,10 +2675,10 @@ public: template Result parse_and_get_value(const char *s, std::any &dt, T &val, - const char *path = nullptr, Log log = nullptr, - ErrorReporter error_reporter = nullptr) const { + const char *path = nullptr, + Log log = nullptr) const { auto n = strlen(s); - return parse_and_get_value(s, n, dt, val, path, log, error_reporter); + return parse_and_get_value(s, n, dt, val, path, log); } #if defined(__cpp_lib_char8_t) @@ -3207,10 +2789,6 @@ public: std::string error_message; bool no_ast_opt = false; - bool no_whitespace = false; // Disable %whitespace skipping inside this rule - // (like a token boundary, without capturing) - std::string ast_name; // When non-empty, AST nodes produced by this rule carry - // this name/tag instead of the rule's own name bool eoi_check = true; @@ -3238,8 +2816,7 @@ private: void initialize_packrat_filter() const; Result parse_core(const char *s, size_t n, SemanticValues &vs, std::any &dt, - const char *path, Log log, - ErrorReporter error_reporter = nullptr) const { + const char *path, Log log) const { initialize_definition_ids(); std::shared_ptr ope = holder_; @@ -3250,28 +2827,22 @@ private: if (tracer_end) { tracer_end(trace_data); } }); - const std::vector *packrat_index = nullptr; - size_t packrat_cached_count = 0; - if (enablePackratParsing) { - initialize_packrat_filter(); - if (!packrat_index_.empty()) { - packrat_index = &packrat_index_; - packrat_cached_count = packrat_cached_count_; - } else { - packrat_cached_count = definition_ids_.size(); - } - } - Context c(path, s, n, definition_ids_.size(), whitespaceOpe, wordOpe, enablePackratParsing, tracer_enter, tracer_leave, trace_data, - verbose_trace, log, error_reporter, packrat_index, - packrat_cached_count); + verbose_trace, log); if (collect_packrat_stats) { packrat_stats_.resize(definition_ids_.size()); c.packrat_stats = &packrat_stats_; } + if (enablePackratParsing) { + initialize_packrat_filter(); + if (!packrat_filter_.empty()) { + c.packrat_rule_filter = &packrat_filter_; + } + } + size_t i = 0; if (whitespaceOpe) { @@ -3310,8 +2881,7 @@ private: mutable std::once_flag definition_ids_init_; mutable std::unordered_map definition_ids_; mutable std::once_flag packrat_filter_init_; - mutable std::vector packrat_index_; // def_id -> cache slot or -1 - mutable size_t packrat_cached_count_ = 0; + mutable std::vector packrat_filter_; }; /* @@ -3325,11 +2895,9 @@ inline size_t parse_literal(const char *s, size_t n, SemanticValues &vs, size_t i = 0; for (; i < lit.size(); i++) { if (i >= n || - (ignore_case - ? (static_cast( - c.tolower_table[static_cast(s[i])]) != - lower_lit[i]) - : (s[i] != lit[i]))) { + (ignore_case ? (static_cast(std::tolower( + static_cast(s[i]))) != lower_lit[i]) + : (s[i] != lit[i]))) { c.set_error_pos(s, lit.data()); return static_cast(-1); } @@ -3382,15 +2950,14 @@ inline std::pair SemanticValues::line_info() const { return c_->line_info(sv_.data()); } -inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, - const char *s, size_t n) { +inline void ErrorInfo::output_log(const Log &log, const char *s, size_t n) { if (message_pos) { if (message_pos > last_output_pos) { last_output_pos = message_pos; auto line = line_info(s, message_pos); std::string msg; - auto unexpected_token = heuristic_error_token(s, n, message_pos); - if (!unexpected_token.empty()) { + if (auto unexpected_token = heuristic_error_token(s, n, message_pos); + !unexpected_token.empty()) { msg = replace_all(message, "%t", unexpected_token); auto unexpected_char = unexpected_token.substr( @@ -3401,28 +2968,13 @@ inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, } else { msg = message; } - if (reporter) { - ErrorReport report; - report.line = line.first; - report.col = line.second; - report.position = static_cast(message_pos - s); - report.unexpected_token = unexpected_token; - report.message = msg; - report.label = label; - reporter(report); - } - if (log) { log(line.first, line.second, msg, label); } + log(line.first, line.second, msg, label); } } else if (error_pos) { if (error_pos > last_output_pos) { last_output_pos = error_pos; auto line = line_info(s, error_pos); - ErrorReport report; - report.line = line.first; - report.col = line.second; - report.position = static_cast(error_pos - s); - std::string msg; if (expected_tokens.empty()) { msg = "syntax error."; @@ -3435,7 +2987,6 @@ inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, msg += ", unexpected '"; msg += unexpected_token; msg += "'"; - report.unexpected_token = unexpected_token; } auto first_item = true; @@ -3450,11 +3001,9 @@ inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, msg += "'"; msg += error_literal; msg += "'"; - report.expected_literals.emplace_back(error_literal); } else { msg += "<" + error_rule->name + ">"; if (label.empty()) { label = error_rule->name; } - report.expected_rules.emplace_back(error_rule->name); } first_item = false; } @@ -3463,11 +3012,7 @@ inline void ErrorInfo::output_log(const Log &log, const ErrorReporter &reporter, } msg += "."; } - if (reporter) { - report.label = label; - reporter(report); - } - if (log) { log(line.first, line.second, msg, label); } + log(line.first, line.second, msg, label); } } } @@ -3482,7 +3027,7 @@ inline size_t Context::skip_whitespace(const char *a_s, size_t n, } inline void Context::set_error_pos(const char *a_s, const char *literal) { - if (log || error_reporter) { + if (log) { if (error_info.error_pos <= a_s) { if (error_info.error_pos < a_s || !error_info.keep_previous_token) { error_info.error_pos = a_s; @@ -3529,7 +3074,7 @@ inline void Context::trace_leave(const Ope &ope, const char *a_s, size_t n, } inline bool Context::is_traceable(const Ope &ope) const { - if (has_tracer) { + if (tracer_enter && tracer_leave) { if (ignore_trace_state) { return false; } return !dynamic_cast(&ope); } @@ -3624,56 +3169,14 @@ inline size_t TokenBoundary::parse_core(const char *s, size_t n, return len; } -// Resolve `%{name}` placeholders in a custom error message against the -// named captures recorded so far ($name<...>). Unknown names resolve to an -// empty string. `%t` / `%c` are resolved later, at log-output time. -inline std::string resolve_capture_placeholders(const std::string &msg, - const Context &c) { - auto pos = msg.find("%{"); - if (pos == std::string::npos) { return msg; } - - std::string r; - size_t i = 0; - while (pos != std::string::npos) { - auto end = msg.find('}', pos + 2); - if (end == std::string::npos) { break; } - r.append(msg, i, pos - i); - auto name = std::string_view(msg).substr(pos + 2, end - (pos + 2)); - for (auto it = c.capture_entries.rbegin(); it != c.capture_entries.rend(); - ++it) { - if (it->first == name) { - // The captured span can include whitespace skipped after a token - // boundary; trim it for display. - auto v = std::string_view(it->second); - while (!v.empty() && - std::isspace(static_cast(v.back()))) { - v.remove_suffix(1); - } - while (!v.empty() && - std::isspace(static_cast(v.front()))) { - v.remove_prefix(1); - } - r += v; - break; - } - } - i = end + 1; - pos = msg.find("%{", i); - } - r.append(msg, i, msg.size() - i); - return r; -} - inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { if (!ope_) { throw std::logic_error("Uninitialized definition ope was used..."); } - // Macro reference. A left-recursive macro cannot take this path: it needs - // the seed-growing below, which in turn needs its own semantic value scope - // to memoise. Such a macro forms a scope like a plain rule does. - if (outer_->is_macro && !outer_->is_left_recursive) { + // Macro reference + if (outer_->is_macro) { c.rule_stack.push_back(outer_); auto len = ope_->parse(s, n, vs, c, dt); c.rule_stack.pop_back(); @@ -3698,23 +3201,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, }); c.rule_stack.push_back(outer_); - if (outer_->no_whitespace) { - { - c.in_token_boundary_count++; - auto se2 = scope_exit([&]() { c.in_token_boundary_count--; }); - parse_len = ope_->parse(s, n, chvs, c, dt); - } - if (success(parse_len)) { - auto wl = c.skip_whitespace(s + parse_len, n - parse_len, chvs, dt); - if (fail(wl)) { - parse_len = wl; - } else { - parse_len += wl; - } - } - } else { - parse_len = ope_->parse(s, n, chvs, c, dt); - } + parse_len = ope_->parse(s, n, chvs, c, dt); c.rule_stack.pop_back(); if (success(parse_len)) { @@ -3734,8 +3221,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, std::any predicate_data; if (outer_->predicate) { if (!outer_->predicate(chvs, dt, msg, predicate_data)) { - if ((c.log || c.error_reporter) && !msg.empty() && - c.error_info.message_pos < s) { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { c.error_info.message_pos = s; c.error_info.message = msg; c.error_info.label = outer_->name; @@ -3747,19 +3233,17 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, if (success(parse_len)) { if (!c.recovered) { parse_val = reduce(chvs, dt, predicate_data); } } else { - if ((c.log || c.error_reporter) && !msg.empty() && - c.error_info.message_pos < s) { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { c.error_info.message_pos = s; c.error_info.message = msg; c.error_info.label = outer_->name; } } } else { - if ((c.log || c.error_reporter) && !outer_->error_message.empty() && + if (c.log && !outer_->error_message.empty() && c.error_info.message_pos < s) { c.error_info.message_pos = s; - c.error_info.message = - resolve_capture_placeholders(outer_->error_message, c); + c.error_info.message = outer_->error_message; c.error_info.label = outer_->name; } } @@ -3768,10 +3252,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, }; if (outer_->is_left_recursive) { - // A macro grows one seed per instantiation: Sum(D) and Sum(L) are - // different rules as far as the memo is concerned. - auto lr_rule = Context::LRRule(outer_, c.top_macro_inst()); - auto lr_key = Context::LRKey(lr_rule, s); + auto lr_key = std::make_pair(outer_, s); // Check LR memo first auto it = c.lr_memo.find(lr_key); @@ -3784,7 +3265,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, } // Record that this rule's lr_memo was accessed. // Any LR rule currently seeding will know we're in its cycle. - c.lr_refs_hit.insert(lr_rule); + c.lr_refs_hit.insert(outer_); } else { // Seed with FAIL c.lr_memo[lr_key] = {static_cast(-1), {}}; @@ -3806,7 +3287,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, // the cycle, so add self — this lets parent seeders see us as // a transitive cycle member. auto cycle_rules = c.lr_refs_hit; - if (!cycle_rules.empty()) { cycle_rules.insert(lr_rule); } + if (!cycle_rules.empty()) { cycle_rules.insert(outer_); } // Restore parent's refs and propagate cycle info upward c.lr_refs_hit = std::move(saved_refs); @@ -3822,17 +3303,15 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, c.lr_memo[lr_key] = {len, val}; while (true) { - // Clear this rule's packrat cache. A macro is never written there - // (that cache is keyed by rule id alone, which cannot tell two - // instantiations apart), so there is nothing to clear for one. - if (!outer_->is_macro) { c.clear_packrat_cache(s, outer_->id); } + // Clear this rule's packrat cache + c.clear_packrat_cache(s, outer_->id); // Clear lr_memo for cycle-dependent rules at this position, // but NOT for rules currently in their own seeding phase // (lr_active_seeds) — those are outer growers we must not // interfere with. for (auto memo_it = c.lr_memo.begin(); memo_it != c.lr_memo.end();) { - if (memo_it->first.second == s && memo_it->first.first != lr_rule && + if (memo_it->first.second == s && memo_it->first.first != outer_ && cycle_rules.count(memo_it->first.first) && !c.lr_active_seeds.count(memo_it->first)) { memo_it = c.lr_memo.erase(memo_it); @@ -3855,9 +3334,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, // Write final result to packrat cache (lr_memo entry is kept as // the primary lookup for LR rules at this position) - if (success(len) && !outer_->is_macro) { - c.write_packrat_cache(s, outer_->id, len, val); - } + if (success(len)) { c.write_packrat_cache(s, outer_->id, len, val); } } } else { if (c.enablePackratParsing) { @@ -3871,7 +3348,7 @@ inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, } else { // Without packrat, use lr_memo as re-entry guard to prevent // stack overflow from undetected left recursion. - auto guard_key = Context::LRKey({outer_, c.top_macro_inst()}, s); + auto guard_key = std::make_pair(outer_, s); if (c.lr_memo.count(guard_key)) { len = static_cast(-1); } else { @@ -3913,23 +3390,6 @@ inline const std::string &Holder::trace_name() const { return trace_name_; } -// Key a macro instantiation by what each argument denotes rather than by the -// node that spells it: `M(N)` written at two call sites builds two Reference -// nodes for the same rule N, and those are the same instantiation. -inline std::vector -macro_inst_key(const Definition *def, - const std::vector> &args) { - std::vector key; - key.reserve(args.size() + 1); - key.push_back(def); - for (const auto &arg : args) { - auto ref = dynamic_cast(arg.get()); - key.push_back(ref && ref->rule_ ? static_cast(ref->rule_) - : static_cast(arg.get())); - } - return key; -} - inline size_t Reference::parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, std::any &dt) const { auto save_ignore_trace_state = c.ignore_trace_state; @@ -3952,10 +3412,7 @@ inline size_t Reference::parse_core(const char *s, size_t n, SemanticValues &vs, args.emplace_back(std::move(vis.found_ope)); } - auto inst = rule_->is_left_recursive - ? c.intern_macro_inst(macro_inst_key(rule_, args)) - : 0; - c.push_args(std::move(args), inst); + c.push_args(std::move(args)); auto se = scope_exit([&]() { c.pop_args(); }); return rule_->holder_->parse(s, n, vs, c, dt); } else { @@ -4091,12 +3548,11 @@ inline size_t Recovery::parse_core(const char *s, size_t n, const auto &rule = dynamic_cast(*ope_); // Custom error message - if (c.log || c.error_reporter) { + if (c.log) { auto label = dynamic_cast(rule.args_[0].get()); if (label && !label->rule_->error_message.empty()) { c.error_info.message_pos = s; - c.error_info.message = - resolve_capture_placeholders(label->rule_->error_message, c); + c.error_info.message = label->rule_->error_message; c.error_info.label = label->rule_->name; } } @@ -4105,13 +3561,8 @@ inline size_t Recovery::parse_core(const char *s, size_t n, auto len = static_cast(-1); { auto save_log = c.log; - auto save_reporter = c.error_reporter; c.log = nullptr; - c.error_reporter = nullptr; - auto se = scope_exit([&]() { - c.log = save_log; - c.error_reporter = save_reporter; - }); + auto se = scope_exit([&]() { c.log = save_log; }); SemanticValues dummy_vs; std::any dummy_dt; @@ -4122,8 +3573,8 @@ inline size_t Recovery::parse_core(const char *s, size_t n, if (success(len)) { c.recovered = true; - if (c.log || c.error_reporter) { - c.error_info.output_log(c.log, c.error_reporter, c.s, c.l); + if (c.log) { + c.error_info.output_log(c.log, c.s, c.l); c.error_info.clear(); } } @@ -4211,37 +3662,32 @@ inline void ComputeCanBeEmpty::visit(Reference &ope) { } inline void DetectLeftRecursion::visit(Reference &ope) { - // Macro parameter reference: what it denotes lives in an enclosing - // instantiation (e.g. B(X) <- C(X) where X is itself a param ref). - auto param = !ope.rule_ && !macro_args_stack_.empty() - ? resolve_macro_arg(ope.iarg_) - : ResolvedArg{}; - if (ope.name_ == name_) { error_s = ope.s_; - } else if (param.ope) { - visit_in_defining_scope(param); - if (done_ == false) { return; } - } else if (ope.is_macro_ && - macro_args_stack_.size() >= max_macro_inst_depth) { - // Unbounded instantiation chain; stop descending. - } else if (ope.rule_ && - refs_ - .emplace(ope.rule_, ope.is_macro_ ? intern_macro_inst(ope) : 0) - .second) { - if (ope.is_macro_) { macro_args_stack_.push_back(&ope.args_); } - ope.rule_->accept(*this); - if (ope.is_macro_) { macro_args_stack_.pop_back(); } - if (done_ == false) { return; } + } else if (!ope.rule_ && !macro_args_stack_.empty()) { + // Macro parameter reference: resolve through nested macro arg + // stacks (e.g. B(X) <- C(X) where X is itself a param ref). + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { + resolved->accept(*this); + if (done_ == false) { return; } + } + } else if (!refs_.count(ope.name_)) { + refs_.insert(ope.name_); + if (ope.rule_) { + if (ope.is_macro_) { macro_args_stack_.push_back(&ope.args_); } + ope.rule_->accept(*this); + if (ope.is_macro_) { macro_args_stack_.pop_back(); } + if (done_ == false) { return; } + } } // If the referenced rule can match empty, don't mark as done — // the sequence may continue past this element to find LR. if (!ope.rule_ && !macro_args_stack_.empty()) { - if (param.ope) { - // ComputeCanBeEmpty never consults the frame stack, so the scope it - // runs in cannot matter. + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { ComputeCanBeEmpty cbe; - param.ope->accept(cbe); + resolved->accept(cbe); done_ = !cbe.result; } else { done_ = true; @@ -4251,51 +3697,20 @@ inline void DetectLeftRecursion::visit(Reference &ope) { } } -inline size_t DetectLeftRecursion::intern_macro_inst(const Reference &ope) { - // Resolve bare parameter references to what the enclosing instantiation was - // given, so a macro passing its own parameter through interns to the same - // instantiation instead of a fresh one at every nesting level. - std::vector> args; - args.reserve(ope.args_.size()); - for (const auto &arg : ope.args_) { - auto ref = dynamic_cast(arg.get()); - auto resolved = ref && !ref->rule_ && !macro_args_stack_.empty() - ? resolve_macro_arg(ref->iarg_).ope - : nullptr; - args.push_back(resolved ? resolved : arg); - } - auto [it, inserted] = macro_inst_ids_.emplace(macro_inst_key(ope.rule_, args), - next_macro_inst_); - if (inserted) { next_macro_inst_++; } - return it->second; -} - -inline void -DetectLeftRecursion::visit_in_defining_scope(const ResolvedArg &arg) { - // The frames below the one holding it are the scope it was written in. - // `W(X) <- Y(X / 'x')` passes Y an argument whose own `X` means W's - // parameter, not Y's -- leaving Y's frame visible would resolve that `X` - // right back to `X / 'x'`, forever. - auto saved = macro_args_stack_; - auto se = scope_exit([&]() { macro_args_stack_ = std::move(saved); }); - macro_args_stack_.resize(arg.depth); - arg.ope->accept(*this); -} - -inline DetectLeftRecursion::ResolvedArg +inline std::shared_ptr DetectLeftRecursion::resolve_macro_arg(size_t iarg) const { for (int i = static_cast(macro_args_stack_.size()) - 1; i >= 0; i--) { auto &args = *macro_args_stack_[i]; - if (iarg >= args.size()) { return {}; } + if (iarg >= args.size()) { return nullptr; } auto ref = dynamic_cast(args[iarg].get()); if (ref && !ref->rule_) { // Another param ref — resolve using parent level's args iarg = ref->iarg_; continue; } - return {args[iarg], static_cast(i)}; + return args[iarg]; } - return {}; + return nullptr; } inline void HasEmptyElement::visit(Sequence &ope) { @@ -4448,16 +3863,8 @@ inline void ComputeFirstSet::visit(Reference &ope) { inline void SetupFirstSets::visit(Reference &ope) { if (!ope.rule_) { return; } - ope.rule_->accept(*this); // re-entry is guarded at the rule's Holder -} - -// Guard rule setup by Definition so a SetupFirstSets shared across all rules -// visits each rule's body at most once for the whole grammar. Without this the -// per-rule setup re-walks every reachable rule once per referencing rule, which -// is O(N^2) for grammars with dense cross-references. -inline void SetupFirstSets::visit(Holder &ope) { - if (!visited_rules_.insert(ope.outer_).second) { return; } - ope.ope_->accept(*this); + if (!visited_rules_.insert(ope.rule_).second) { return; } + ope.rule_->accept(*this); } inline void SetupFirstSets::visit(Sequence &ope) { @@ -4575,47 +3982,17 @@ inline void Definition::initialize_packrat_filter() const { auto def_count = definition_ids_.size(); if (def_count == 0) { return; } - // Collect rule IDs that can be invoked at the *same start position* as - // the given Ope subtree (leftmost reachability). A packrat cache hit - // requires the same rule to be queried twice at the same position, and - // in a PEG that only happens when alternatives of a choice share a - // leftmost prefix — rules reachable only past a consuming element can - // never be re-queried by a sibling alternative. - struct CollectLeftmostRules : public TraversalVisitor { + // Collect rule IDs reachable from an Ope subtree (bitvector indexed by + // def_id) + struct CollectReachableRules : public TraversalVisitor { using TraversalVisitor::visit; std::vector reachable; // indexed by def_id - std::vector - visited_rules; // indexed by def_id; guards Holder cycles - CollectLeftmostRules(size_t n) - : reachable(n, false), visited_rules(n, false) {} + CollectReachableRules(size_t n) : reachable(n, false) {} - // Collect from the position element `from` starts at: element `from` - // itself, plus what follows for as long as elements can match empty — - // only up to (and including) the first one that must consume input. - void collect(const std::vector> &opes, size_t from) { - for (auto i = from; i < opes.size(); i++) { - opes[i]->accept(*this); - ComputeCanBeEmpty empty_vis; - opes[i]->accept(empty_vis); - if (!empty_vis.result) { break; } - } - } - - void visit(Sequence &ope) override { collect(ope.opes_, 0); } void visit(Holder &ope) override { auto id = ope.outer_->id; - if (id < reachable.size()) { - reachable[id] = true; - - // Grammars built directly via the combinator API embed rules through - // WeakHolder rather than Reference, so a recursive rule forms a - // Holder cycle with no Reference to break it. Guard re-entry to avoid - // infinite recursion (reachability is monotone, so revisiting a rule - // we have already traversed adds nothing). - if (visited_rules[id]) { return; } - visited_rules[id] = true; - } + if (id < reachable.size()) { reachable[id] = true; } ope.ope_->accept(*this); } void visit(Reference &ope) override { @@ -4627,8 +4004,7 @@ inline void Definition::initialize_packrat_filter() const { } }; - // Find rules that benefit: queried by 2+ alternatives of the same choice - // at the same position + // Find rules that benefit: reachable from 2+ alternatives of same choice std::vector benefits(def_count, false); struct FindBacktrackRules : public TraversalVisitor { @@ -4640,60 +4016,24 @@ inline void Definition::initialize_packrat_filter() const { FindBacktrackRules(std::vector &b, size_t n) : benefits(b), def_count(n), visited_rules(n, false) {} - using Elements = std::vector>; - - // An alternative's top-level elements, so a shared prefix can be walked - // element by element. By value: this runs once per grammar. - static Elements elements_of(const std::shared_ptr &alt) { - if (auto *seq = dynamic_cast(alt.get())) { - return seq->opes_; + void visit(PrioritizedChoice &ope) override { + // For each alternative, collect reachable rules as bitvectors + std::vector> alt_reachable; + for (auto &op : ope.opes_) { + CollectReachableRules crr(def_count); + op->accept(crr); + alt_reachable.push_back(std::move(crr.reachable)); } - return {alt}; - } - // `group` holds alternatives that agree on their first `k` elements, so - // every one of them reaches element k at the same input position — that - // is exactly when a packrat cache entry can hit. k == 0 is the plain - // "alternatives of one choice" case; deeper k is what a shared prefix - // like `'(' _ PATTERN _ ',' _` hides. - void mark_aligned(const std::vector &group, size_t k) { - if (group.size() < 2) { return; } - - std::vector> reachable; - reachable.reserve(group.size()); - for (const auto &seq : group) { - CollectLeftmostRules clr(def_count); - clr.collect(seq, k); - reachable.push_back(std::move(clr.reachable)); - } + // Mark rules reachable from 2+ alternatives for (size_t id = 0; id < def_count; id++) { size_t count = 0; - for (const auto &alt : reachable) { + for (auto &alt : alt_reachable) { if (alt[id]) { count++; } } if (count >= 2) { benefits[id] = true; } } - // Only alternatives that also agree on element k stay aligned past it. - std::map> aligned; - for (const auto &seq : group) { - if (k < seq.size()) { - aligned[OpeSignature::get(*seq[k])].push_back(seq); - } - } - for (const auto &[sig, sub] : aligned) { - mark_aligned(sub, k + 1); - } - } - - void visit(PrioritizedChoice &ope) override { - std::vector group; - group.reserve(ope.opes_.size()); - for (const auto &op : ope.opes_) { - group.push_back(elements_of(op)); - } - mark_aligned(group, 0); - // Recurse into alternatives for (auto &op : ope.opes_) { op->accept(*this); @@ -4716,23 +4056,7 @@ inline void Definition::initialize_packrat_filter() const { if (whitespaceOpe) { whitespaceOpe->accept(finder); } if (wordOpe) { wordOpe->accept(finder); } - // Left-recursive rules read and write the packrat cache directly during - // seed-growing, so they must stay in the cached set. Macros are the - // exception: they use lr_memo only, keyed by instantiation. - for (const auto &[ptr, id] : definition_ids_) { - auto *def = static_cast(ptr); - if (def->is_left_recursive && !def->is_macro && id < def_count) { - benefits[id] = true; - } - } - - // Compact index: def_id -> slot in the cache tables (-1 = guard only) - packrat_index_.assign(def_count, -1); - int32_t k = 0; - for (size_t id = 0; id < def_count; id++) { - if (benefits[id]) { packrat_index_[id] = k++; } - } - packrat_cached_count_ = static_cast(k); + packrat_filter_ = std::move(benefits); }); } @@ -4770,419 +4094,6 @@ inline void FindReference::visit(Reference &ope) { found_ope = ope.shared_from_this(); } -/*----------------------------------------------------------------------------- - * Grammar serialization - * - * Serialize a compiled Grammar (the operator tree) to a byte blob and back, - * letting an application skip the meta-parse on startup by embedding a - * prebuilt blob. Structure only: semantic callbacks (actions / enter / leave / - * predicate, attached by enable_ast() etc.) are NOT serialized and must be - * re-applied after deserialize. References resolve by name (no pointer fixup); - * first-sets and keyword guards are recomputed on load (O(N)). The - * `precedence` instruction is supported (its operator table is structural). - * Grammars using the `User` operator or a Capture with a match action are - * rejected. The blob is specific to this peglib version's layout. - *---------------------------------------------------------------------------*/ - -struct GrammarBlob { - enum Tag : uint8_t { - T_Sequence, - T_Choice, - T_Repetition, - T_And, - T_Not, - T_Dictionary, - T_Literal, - T_CharClass, - T_Char, - T_AnyChar, - T_CaptureScope, - T_Capture, - T_TokenBoundary, - T_Ignore, - T_BackRef, - T_Reference, - T_Whitespace, - T_Recovery, - T_Cut, - T_PrecedenceClimbing, - T_Null - }; - - struct Writer { - std::vector b; - void u8(uint8_t v) { b.push_back(v); } - void u32(uint32_t v) { - for (int i = 0; i < 4; i++) - b.push_back((v >> (8 * i)) & 0xff); - } - void u64(uint64_t v) { - for (int i = 0; i < 8; i++) - b.push_back((v >> (8 * i)) & 0xff); - } - void str(const std::string &s) { - u32((uint32_t)s.size()); - b.insert(b.end(), s.begin(), s.end()); - } - }; - - static void write_ope(Writer &w, const std::shared_ptr &o) { - if (!o) { - w.u8(T_Null); - return; - } - Ope *p = o.get(); - if (auto x = dynamic_cast(p)) { - w.u8(T_Sequence); - w.u32((uint32_t)x->opes_.size()); - for (auto &c : x->opes_) - write_ope(w, c); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Choice); - w.u8(x->for_label_ ? 1 : 0); - w.u32((uint32_t)x->opes_.size()); - for (auto &c : x->opes_) - write_ope(w, c); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Repetition); - w.u64(x->min_); - w.u64(x->max_); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_And); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Not); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Dictionary); - w.u8(x->trie_.ignore_case_ ? 1 : 0); - // Recover words in their original choice-index order. The Trie stores - // each full word's id (its index in the constructor vector), which - // parse_core reports as vs.choice(). Iterating dic_ directly yields - // sorted key order and would renumber the choices, so place each word at - // its id. - std::vector words(x->trie_.items_count()); - for (auto &kv : x->trie_.dic_) - if (kv.second.match && kv.second.id < words.size()) - words[kv.second.id] = kv.first; - w.u32((uint32_t)words.size()); - for (auto &s : words) - w.str(s); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Literal); - w.u8(x->ignore_case_ ? 1 : 0); - w.str(x->lit_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_CharClass); - w.u8(x->negated_ ? 1 : 0); - w.u8(x->ignore_case_ ? 1 : 0); - w.u32((uint32_t)x->ranges_.size()); - for (auto &r : x->ranges_) { - w.u32((uint32_t)r.first); - w.u32((uint32_t)r.second); - } - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Char); - w.u32((uint32_t)x->ch_); - } else if (dynamic_cast(p)) { - w.u8(T_AnyChar); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_CaptureScope); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - if (x->match_action_) { - throw std::runtime_error( - "GrammarBlob: Capture with a match action is not serializable"); - } - w.u8(T_Capture); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_TokenBoundary); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Ignore); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_BackRef); - w.str(x->name_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Reference); - w.u8(x->is_macro_ ? 1 : 0); - w.str(x->name_); - w.u32((uint32_t)x->args_.size()); - for (auto &a : x->args_) - write_ope(w, a); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Whitespace); - write_ope(w, x->ope_); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_Recovery); - write_ope(w, x->ope_); - } else if (dynamic_cast(p)) { - w.u8(T_Cut); - } else if (auto x = dynamic_cast(p)) { - w.u8(T_PrecedenceClimbing); - write_ope(w, x->atom_); - write_ope(w, x->binop_); - w.u32((uint32_t)x->info_.size()); - for (auto &[key, pri] : x->info_) { - w.str(std::string(key)); - w.u64((uint64_t)pri.first); - w.u8((uint8_t)pri.second); - } - } else { - throw std::runtime_error( - "GrammarBlob: operator not serializable (a custom User operator or " - "a Capture with a match action)"); - } - } - - struct Reader { - const uint8_t *p, *end; - uint8_t u8() { - if (p >= end) - throw std::runtime_error("GrammarBlob: unexpected end of blob"); - return *p++; - } - uint32_t u32() { - uint32_t v = 0; - for (int i = 0; i < 4; i++) - v |= (uint32_t)u8() << (8 * i); - return v; - } - uint64_t u64() { - uint64_t v = 0; - for (int i = 0; i < 8; i++) - v |= (uint64_t)u8() << (8 * i); - return v; - } - std::string str() { - uint32_t n = u32(); - std::string s((const char *)p, (const char *)p + n); - p += n; - return s; - } - }; - - static std::shared_ptr read_ope(Reader &r, Grammar &g, - Definition *owner) { - switch (r.u8()) { - case T_Null: return nullptr; - case T_Sequence: { - uint32_t n = r.u32(); - std::vector> v; - for (uint32_t i = 0; i < n; i++) - v.push_back(read_ope(r, g, owner)); - return std::make_shared(std::move(v)); - } - case T_Choice: { - bool fl = r.u8(); - uint32_t n = r.u32(); - std::vector> v; - for (uint32_t i = 0; i < n; i++) - v.push_back(read_ope(r, g, owner)); - auto c = std::make_shared(std::move(v)); - c->for_label_ = fl; - return c; - } - case T_Repetition: { - uint64_t mn = r.u64(), mx = r.u64(); - auto o = read_ope(r, g, owner); - return std::make_shared(o, mn, mx); - } - case T_And: return std::make_shared(read_ope(r, g, owner)); - case T_Not: return std::make_shared(read_ope(r, g, owner)); - case T_Dictionary: { - bool ic = r.u8(); - uint32_t n = r.u32(); - std::vector words; - for (uint32_t i = 0; i < n; i++) - words.push_back(r.str()); - return std::make_shared(words, ic); - } - case T_Literal: { - bool ic = r.u8(); - std::string s = r.str(); - return std::make_shared(std::move(s), ic); - } - case T_CharClass: { - bool neg = r.u8(), ic = r.u8(); - uint32_t n = r.u32(); - std::vector> ranges; - for (uint32_t i = 0; i < n; i++) { - auto lo = r.u32(), hi = r.u32(); - ranges.emplace_back((char32_t)lo, (char32_t)hi); - } - return std::make_shared(ranges, neg, ic); - } - case T_Char: return std::make_shared((char32_t)r.u32()); - case T_AnyChar: return std::make_shared(); - case T_CaptureScope: - return std::make_shared(read_ope(r, g, owner)); - case T_Capture: { - auto o = read_ope(r, g, owner); - return std::make_shared(o, nullptr); - } - case T_TokenBoundary: - return std::make_shared(read_ope(r, g, owner)); - case T_Ignore: return std::make_shared(read_ope(r, g, owner)); - case T_BackRef: return std::make_shared(r.str()); - case T_Reference: { - bool im = r.u8(); - std::string nm = r.str(); - uint32_t n = r.u32(); - std::vector> args; - for (uint32_t i = 0; i < n; i++) - args.push_back(read_ope(r, g, owner)); - return std::make_shared(g, nm, nullptr, im, args); - } - case T_Whitespace: - return std::make_shared(read_ope(r, g, owner)); - case T_Recovery: return std::make_shared(read_ope(r, g, owner)); - case T_Cut: return std::make_shared(); - case T_PrecedenceClimbing: { - if (!owner) { - throw std::runtime_error( - "GrammarBlob: 'precedence' operator outside a rule body"); - } - auto atom = read_ope(r, g, owner); - auto binop = read_ope(r, g, owner); - uint32_t n = r.u32(); - auto pc = std::make_shared( - atom, binop, PrecedenceClimbing::BinOpeInfo{}, *owner); - // info_ keys are string_views; back them with owned strings whose - // addresses stay stable (reserve avoids reallocation, and the node is - // never moved once held by shared_ptr). - pc->info_keys_.reserve(n); - for (uint32_t i = 0; i < n; i++) { - std::string key = r.str(); - auto level = (size_t)r.u64(); - auto assoc = (char)r.u8(); - pc->info_keys_.push_back(std::move(key)); - pc->info_[pc->info_keys_.back()] = std::pair(level, assoc); - } - return pc; - } - default: throw std::runtime_error("GrammarBlob: bad operator tag"); - } - } - - static const uint32_t MAGIC = 0x50454732; // "PEG2" - - static std::vector serialize(const Grammar &g, - const std::string &start) { - Writer w; - w.u32(MAGIC); - w.str(start); - w.u32((uint32_t)g.size()); - // Grammar is an unordered_map, whose iteration order is implementation - // defined: walking it directly yields different bytes for the same grammar - // on different standard libraries, so a blob generated on one platform - // cannot be byte-compared on another. Emit the definitions by name. - // deserialize() rebuilds the map from the names, so the order carries no - // meaning of its own. - std::vector defs; - defs.reserve(g.size()); - for (auto &kv : g) - defs.push_back(&kv); - std::sort(defs.begin(), defs.end(), - [](const auto *a, const auto *b) { return a->first < b->first; }); - for (auto *kv : defs) { - const auto &name = kv->first; - const auto &def = kv->second; - w.str(name); - uint8_t flags = - (def.ignoreSemanticValue ? 1 : 0) | (def.is_macro ? 2 : 0) | - (def.no_ast_opt ? 4 : 0) | (def.eoi_check ? 8 : 0) | - (def.enablePackratParsing ? 16 : 0) | - (def.is_left_recursive ? 32 : 0) | (def.can_be_empty ? 64 : 0) | - (def.disable_action ? 128 : 0); - w.u8(flags); - uint8_t flags2 = (def.no_whitespace ? 1 : 0); - w.u8(flags2); - w.u32((uint32_t)def.params.size()); - for (auto &s : def.params) - w.str(s); - w.str(def.ast_name); - w.str(def.error_message); - write_ope(w, const_cast(def).get_core_operator()); - } - return std::move(w.b); - } - - static std::shared_ptr deserialize(const std::vector &blob, - std::string &start_out) { - Reader r{blob.data(), blob.data() + blob.size()}; - if (r.u32() != MAGIC) - throw std::runtime_error("GrammarBlob: bad magic / not a grammar blob"); - start_out = r.str(); - uint32_t ndef = r.u32(); - auto g = std::make_shared(); - // Create each Definition before reading its body: a PrecedenceClimbing node - // needs a stable reference to its owning rule at construction. Grammar is a - // node-based map, so references stay valid as later rules are inserted. - for (uint32_t i = 0; i < ndef; i++) { - std::string name = r.str(); - uint8_t flags = r.u8(); - uint8_t flags2 = r.u8(); - uint32_t np = r.u32(); - std::vector params; - for (uint32_t k = 0; k < np; k++) - params.push_back(r.str()); - std::string ast_name = r.str(); - std::string err = r.str(); - - auto &def = (*g)[name]; - def.name = name; - def.ignoreSemanticValue = flags & 1; - def.is_macro = flags & 2; - def.no_ast_opt = flags & 4; - def.eoi_check = flags & 8; - def.enablePackratParsing = flags & 16; - def.is_left_recursive = flags & 32; - def.can_be_empty = flags & 64; - def.disable_action = flags & 128; - def.no_whitespace = flags2 & 1; - def.params = std::move(params); - def.ast_name = std::move(ast_name); - def.error_message = std::move(err); - - auto body = read_ope(r, *g, &def); - def <= body; - } - for (auto &x : *g) { - LinkReferences vis(*g, x.second.params); - x.second.accept(vis); - // TraversalVisitor descends only into a PrecedenceClimbing's atom_. In - // the from-source path binop_ is linked while the body is still a - // Sequence, before precedence lowering; a deserialized node is built - // already lowered so its binop_ reference must be linked explicitly here. - auto core = x.second.get_core_operator(); - if (auto pc = std::dynamic_pointer_cast(core)) { - pc->binop_->accept(vis); - } - } - { - SetupFirstSets vis; // shared across rules -> O(N) - for (auto &x : *g) - x.second.accept(vis); - } - // Re-derive automatic whitespace/word skipping on the start rule from the - // %whitespace / %word definitions, exactly as ParserGenerator does. Sharing - // the (already linked and first-set) definition operators avoids leaving - // references inside the skipping ope unlinked, and keeps the blob smaller. - if (g->count(WHITESPACE_DEFINITION_NAME)) { - (*g)[start_out].whitespaceOpe = - wsp((*g)[WHITESPACE_DEFINITION_NAME].get_core_operator()); - } - if (g->count(WORD_DEFINITION_NAME)) { - (*g)[start_out].wordOpe = (*g)[WORD_DEFINITION_NAME].get_core_operator(); - } - return g; - } -}; - /*----------------------------------------------------------------------------- * PEG parser generator *---------------------------------------------------------------------------*/ @@ -5229,17 +4140,6 @@ private: ParserGenerator() { make_grammar(); setup_actions(); - // Apply First-Set filtering to the bootstrap meta-grammar itself so that - // parsing a grammar (the bulk of load_grammar) skips alternatives whose - // next byte cannot match. This is safe -- First-Set filtering only skips - // alternatives that would have failed anyway, so no semantic action that - // would have committed is skipped (unlike packrat, which is unsound here). - { - SetupFirstSets vis; - for (auto &x : g) { - x.second.accept(vis); - } - } } struct Instruction { @@ -5281,13 +4181,11 @@ private: void make_grammar() { // Setup PEG syntax parser g["Grammar"] <= seq(g["Spacing"], oom(g["Definition"]), g["EndOfFile"]); - // Left-factored: parse the rule name (IdentCont) once, then optionally the - // macro parameter list. `opt(Parameters)` pushes a value only for a macro - // (so the value layout matches the old two-alternative form), and Spacing - // (~, no value) consumes the gap before LEFTARROW that Identifier used to. - g["Definition"] <= seq(g["Ignore"], g["IdentCont"], opt(g["Parameters"]), - g["Spacing"], g["LEFTARROW"], g["Expression"], - opt(g["Instruction"])); + g["Definition"] <= + cho(seq(g["Ignore"], g["IdentCont"], g["Parameters"], g["LEFTARROW"], + g["Expression"], opt(g["Instruction"])), + seq(g["Ignore"], g["Identifier"], g["LEFTARROW"], g["Expression"], + opt(g["Instruction"]))); g["Expression"] <= seq(g["Sequence"], zom(seq(g["SLASH"], g["Sequence"]))); g["Sequence"] <= zom(cho(g["CUT"], g["Prefix"])); g["Prefix"] <= seq(opt(cho(g["AND"], g["NOT"])), g["SuffixWithLabel"]); @@ -5295,19 +4193,17 @@ private: seq(g["Suffix"], opt(seq(g["LABEL"], g["Identifier"]))); g["Suffix"] <= seq(g["Primary"], opt(g["Loop"])); g["Loop"] <= cho(g["QUESTION"], g["STAR"], g["PLUS"], g["Repetition"]); - // Left-factored: a macro reference (`Name(args)`) and a plain reference - // (`Name`) share the leading `Ignore IdentCont`, so parse it once and let - // `opt(Arguments)` decide. opt() pushes the argument list only for a macro - // reference, so vs.size() distinguishes the two in the action. - g["Primary"] <= - cho(seq(g["Ignore"], g["IdentCont"], opt(g["Arguments"]), g["Spacing"], - npd(seq(opt(g["Parameters"]), g["LEFTARROW"]))), - seq(g["OPEN"], g["Expression"], g["CLOSE"]), - seq(g["BeginTok"], g["Expression"], g["EndTok"]), g["CapScope"], - seq(g["BeginCap"], g["Expression"], g["EndCap"]), g["BackRef"], - g["DictionaryI"], g["LiteralI"], g["Dictionary"], g["Literal"], - g["NegatedClassI"], g["NegatedClass"], g["ClassI"], g["Class"], - g["DOT"]); + g["Primary"] <= cho(seq(g["Ignore"], g["IdentCont"], g["Arguments"], + npd(g["LEFTARROW"])), + seq(g["Ignore"], g["Identifier"], + npd(seq(opt(g["Parameters"]), g["LEFTARROW"]))), + seq(g["OPEN"], g["Expression"], g["CLOSE"]), + seq(g["BeginTok"], g["Expression"], g["EndTok"]), + g["CapScope"], + seq(g["BeginCap"], g["Expression"], g["EndCap"]), + g["BackRef"], g["DictionaryI"], g["LiteralI"], + g["Dictionary"], g["Literal"], g["NegatedClassI"], + g["NegatedClass"], g["ClassI"], g["Class"], g["DOT"]); g["Identifier"] <= seq(g["IdentCont"], g["Spacing"]); g["IdentCont"] <= tok(seq(g["IdentStart"], zom(g["IdentRest"]))); @@ -5356,12 +4252,8 @@ private: // NOTE: This is different from The original Brian Ford's paper, and this // modification allows us to specify `[+-]` as a valid char class. - g["Range"] <= cho(seq(g["Char"], chr('-'), npd(chr(']')), g["Char"]), - g["ClassEscape"], g["PosixClass"], g["Char"]); - - g["ClassEscape"] <= seq(chr('\\'), cls("dDwWsS")); - g["PosixClass"] <= - seq(lit("[:"), opt(chr('^')), oom(cls("a-z")), lit(":]")); + g["Range"] <= + cho(seq(g["Char"], chr('-'), npd(chr(']')), g["Char"]), g["Char"]); g["Char"] <= cho(seq(chr('\\'), cls("fnrtv'\"[]\\^-")), @@ -5431,8 +4323,8 @@ private: opt(seq(g["InstructionItem"], zom(seq(g["InstructionItemSeparator"], g["InstructionItem"])))), g["EndBracket"]); - g["InstructionItem"] <= cho(g["PrecedenceClimbing"], g["ErrorMessage"], - g["NoAstOpt"], g["NoWhitespace"], g["AstName"]); + g["InstructionItem"] <= + cho(g["PrecedenceClimbing"], g["ErrorMessage"], g["NoAstOpt"]); ~g["InstructionItemSeparator"] <= seq(chr(';'), g["Spacing"]); ~g["SpacesZom"] <= zom(g["Space"]); @@ -5465,13 +4357,6 @@ private: // No Ast node optimization instruction g["NoAstOpt"] <= seq(lit("no_ast_opt"), g["SpacesZom"]); - // No whitespace skipping instruction - g["NoWhitespace"] <= seq(lit("no_whitespace"), g["SpacesZom"]); - - // AST node name override instruction: `{ ast_name: NodeTag }` - g["AstName"] <= seq(lit("ast_name"), g["SpacesZom"], lit(":"), - g["SpacesZom"], g["Identifier"], g["SpacesZom"]); - // Set definition names for (auto &x : g) { x.second.name = x.first; @@ -5482,10 +4367,7 @@ private: g["Definition"] = [&](const SemanticValues &vs, std::any &dt) { auto &data = *std::any_cast(dt); - // Macro iff the optional Parameters matched: its value (the parameter - // name list) then sits at vs[2]. A plain definition has LEFTARROW's value - // there instead. - auto is_macro = vs[2].type() == typeid(std::vector); + auto is_macro = vs.choice() == 0; auto ignore = std::any_cast(vs[0]); auto name = std::any_cast(vs[1]); @@ -5511,6 +4393,9 @@ private: if (types.find(type) == types.end()) { data.instructions[name].push_back(instruction); types.insert(instruction.type); + if (type == "declare_symbol" || type == "check_symbol") { + if (!TokenChecker::is_token(*ope)) { ope = tok(ope); } + } } else { data.duplicates_of_instruction.emplace_back(type, instruction.sv.data()); @@ -5529,9 +4414,7 @@ private: rule.is_macro = is_macro; rule.params = params; - // Reserved `%`-prefixed rules (%whitespace, %word, ...) are directives, - // not parseable entry points, so they must not become the start rule. - if (data.start.empty() && name[0] != '%') { + if (data.start.empty()) { data.start = rule.name; data.start_pos = rule.s_; } @@ -5648,9 +4531,9 @@ private: auto &data = *std::any_cast(dt); switch (vs.choice()) { - case 0: { // Reference / Macro reference (left-factored) - // Macro reference iff opt(Arguments) matched and pushed the arg list. - auto is_macro = vs.size() > 2; + case 0: // Macro Reference + case 1: { // Reference + auto is_macro = vs.choice() == 0; auto ignore = std::any_cast(vs[0]); const auto &ident = std::any_cast(vs[1]); @@ -5668,16 +4551,16 @@ private: return ope; } } - case 1: { // (Expression) + case 2: { // (Expression) return std::any_cast>(vs[0]); } - case 2: { // TokenBoundary + case 3: { // TokenBoundary return tok(std::any_cast>(vs[0])); } - case 3: { // CaptureScope + case 4: { // CaptureScope return csc(std::any_cast>(vs[0])); } - case 4: { // Capture + case 5: { // Capture const auto &name = std::any_cast(vs[0]); auto ope = std::any_cast>(vs[1]); @@ -5724,36 +4607,23 @@ private: return resolve_escape_sequence(tok.data(), tok.size()); }; - // A Range produces either a single range (std::pair) or a range list - // (std::vector) for `\d`-style escapes and POSIX classes. - auto collect_ranges = [](const SemanticValues &vs) { - std::vector> ranges; - for (const auto &v : vs) { - if (v.type() == typeid(std::pair)) { - ranges.push_back(std::any_cast>(v)); - } else { - const auto &vec = - std::any_cast> &>( - v); - ranges.insert(ranges.end(), vec.begin(), vec.end()); - } - } - return ranges; + g["Class"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges); }; - - g["Class"] = [collect_ranges](const SemanticValues &vs) { - return cls(collect_ranges(vs)); + g["ClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges, true); }; - g["ClassI"] = [collect_ranges](const SemanticValues &vs) { - return cls(collect_ranges(vs), true); + g["NegatedClass"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges); }; - g["NegatedClass"] = [collect_ranges](const SemanticValues &vs) { - return ncls(collect_ranges(vs)); + g["NegatedClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges, true); }; - g["NegatedClassI"] = [collect_ranges](const SemanticValues &vs) { - return ncls(collect_ranges(vs), true); - }; - g["Range"] = [](const SemanticValues &vs) -> std::any { + g["Range"] = [](const SemanticValues &vs) { switch (vs.choice()) { case 0: { auto s1 = std::any_cast(vs[0]); @@ -5766,10 +4636,7 @@ private: } return std::pair(cp1, cp2); } - case 1: // ClassEscape - case 2: // PosixClass - return vs[0]; - case 3: { + case 1: { auto s = std::any_cast(vs[0]); auto cp = decode_codepoint(s.data(), s.length()); return std::pair(cp, cp); @@ -5777,33 +4644,6 @@ private: } return std::pair(0, 0); }; - g["ClassEscape"] = [](const SemanticValues &vs) { - auto ch = vs.sv()[1]; - const char *name = nullptr; - switch (ch) { - case 'd': - case 'D': name = "digit"; break; - case 's': - case 'S': name = "space"; break; - default: name = "word"; break; - } - auto ranges = *predefined_character_class(name); - if (ch == 'D' || ch == 'S' || ch == 'W') { - ranges = complement_character_ranges(ranges); - } - return ranges; - }; - g["PosixClass"] = [](const SemanticValues &vs) { - auto sv = vs.sv(); // `[:name:]` or `[:^name:]` - auto negated = sv[2] == '^'; - auto name = sv.substr(negated ? 3 : 2, sv.size() - (negated ? 5 : 4)); - auto ranges = predefined_character_class(name); - if (!ranges) { - auto msg = "invalid POSIX character class '" + std::string(name) + "'"; - throw SyntaxErrorException(msg.c_str(), vs.line_info()); - } - return negated ? complement_character_ranges(*ranges) : *ranges; - }; g["Char"] = [](const SemanticValues &vs) { return resolve_escape_sequence(vs.sv().data(), vs.sv().length()); }; @@ -5933,21 +4773,6 @@ private: return instruction; }; - g["NoWhitespace"] = [](const SemanticValues &vs) { - Instruction instruction; - instruction.type = "no_whitespace"; - instruction.sv = vs.sv(); - return instruction; - }; - - g["AstName"] = [](const SemanticValues &vs) { - Instruction instruction; - instruction.type = "ast_name"; - instruction.data = std::any_cast(vs[0]); - instruction.sv = vs.sv(); - return instruction; - }; - g["Instruction"] = [](const SemanticValues &vs) { return vs.transform(); }; @@ -6251,23 +5076,14 @@ private: rule.error_message = std::any_cast(instruction.data); } else if (instruction.type == "no_ast_opt") { rule.no_ast_opt = true; - } else if (instruction.type == "no_whitespace") { - rule.no_whitespace = true; - } else if (instruction.type == "ast_name") { - rule.ast_name = std::any_cast(instruction.data); } } } - // Setup First-Set and ISpan optimizations. A single visitor is shared - // across all rules so its first-set cache and visited-rule set persist: - // each rule's first-sets are computed once (O(N)) instead of re-walking - // every reachable rule once per referencing rule (O(N^2)). - { + // Setup First-Set and ISpan optimizations + for (auto &x : grammar) { SetupFirstSets vis; - for (auto &x : grammar) { - x.second.accept(vis); - } + x.second.accept(vis); } return {data.grammar, start, data.enablePackratParsing}; @@ -6301,24 +5117,21 @@ template struct AstBase : public Annotation { AstBase(const char *path, size_t line, size_t column, const char *name, const std::vector> &nodes, size_t position = 0, size_t length = 0, size_t choice_count = 0, - size_t choice = 0, bool preserve_position = false) + size_t choice = 0) : path(path ? path : ""), line(line), column(column), name(name), position(position), length(length), choice_count(choice_count), choice(choice), original_name(name), original_choice_count(choice_count), original_choice(choice), - tag(str2tag(name)), original_tag(tag), is_token(false), - preserve_position(preserve_position), nodes(nodes) {} + tag(str2tag(name)), original_tag(tag), is_token(false), nodes(nodes) {} AstBase(const char *path, size_t line, size_t column, const char *name, const std::string_view &token, size_t position = 0, size_t length = 0, - size_t choice_count = 0, size_t choice = 0, - bool preserve_position = false) + size_t choice_count = 0, size_t choice = 0) : path(path ? path : ""), line(line), column(column), name(name), position(position), length(length), choice_count(choice_count), choice(choice), original_name(name), original_choice_count(choice_count), original_choice(choice), - tag(str2tag(name)), original_tag(tag), is_token(true), - preserve_position(preserve_position), token(token) {} + tag(str2tag(name)), original_tag(tag), is_token(true), token(token) {} AstBase(const AstBase &ast, const char *original_name, size_t position = 0, size_t length = 0, size_t original_choice_count = 0, @@ -6329,8 +5142,7 @@ template struct AstBase : public Annotation { original_choice_count(original_choice_count), original_choice(original_choice), tag(ast.tag), original_tag(str2tag(original_name)), is_token(ast.is_token), - preserve_position(ast.preserve_position), token(ast.token), - nodes(ast.nodes), parent(ast.parent) {} + token(ast.token), nodes(ast.nodes), parent(ast.parent) {} const std::string path; const size_t line = 1; @@ -6348,7 +5160,6 @@ template struct AstBase : public Annotation { const unsigned int original_tag; const bool is_token; - const bool preserve_position; const std::string_view token; std::vector>> nodes; @@ -6411,10 +5222,8 @@ struct AstOptimizer { if (opt && original->nodes.size() == 1) { auto child = optimize(original->nodes[0], parent); - auto pos = - child->preserve_position ? child->position : original->position; - auto len = child->preserve_position ? child->length : original->length; - auto ast = std::make_shared(*child, original->name.data(), pos, len, + auto ast = std::make_shared(*child, original->name.data(), + original->position, original->length, original->choice_count, original->choice); for (auto &node : ast->nodes) { node->parent = ast; @@ -6444,23 +5253,18 @@ template void add_ast_action(Definition &rule) { rule.action = [&](const SemanticValues &vs) { auto line = vs.line_info(); - // `{ ast_name: X }` overrides the node's name/tag (falls back to the - // rule's own name when unset). - const char *node_name = - rule.ast_name.empty() ? rule.name.data() : rule.ast_name.data(); - if (rule.is_token()) { return std::make_shared( - vs.path, line.first, line.second, node_name, vs.token(), + vs.path, line.first, line.second, rule.name.data(), vs.token(), std::distance(vs.ss, vs.sv().data()), vs.sv().length(), - vs.choice_count(), vs.choice(), rule.no_ast_opt); + vs.choice_count(), vs.choice()); } - auto ast = std::make_shared(vs.path, line.first, line.second, node_name, - vs.transform>(), - std::distance(vs.ss, vs.sv().data()), - vs.sv().length(), vs.choice_count(), - vs.choice(), rule.no_ast_opt); + auto ast = + std::make_shared(vs.path, line.first, line.second, rule.name.data(), + vs.transform>(), + std::distance(vs.ss, vs.sv().data()), + vs.sv().length(), vs.choice_count(), vs.choice()); for (auto &node : ast->nodes) { node->parent = ast; @@ -6657,32 +5461,10 @@ public: return load_grammar(sv.data(), sv.size(), Rules(), start); } - // Serialize the loaded grammar to a portable byte blob (see GrammarBlob). - // Semantic callbacks are not included; throws if the grammar is not - // serializable (uses the `User` operator or a Capture with a match action). - std::vector serialize_grammar() const { - return GrammarBlob::serialize(*grammar_, start_); - } - - // Load a grammar from a blob produced by serialize_grammar() / GrammarBlob, - // skipping the meta-parse. Re-apply enable_ast() etc. afterwards as needed. - bool load_blob(const std::vector &blob) { - try { - grammar_ = GrammarBlob::deserialize(blob, start_); - } catch (const std::exception &) { return false; } - if (grammar_ != nullptr) { - // Symmetry with load_grammar(): restore the parser-level packrat flag - // from the blob so a later enable_packrat_parsing() re-applies it - // instead of resetting the start rule to the false member default. - enablePackratParsing_ = (*grammar_)[start_].enablePackratParsing; - } - return grammar_ != nullptr; - } - bool parse_n(const char *s, size_t n, const char *path = nullptr) const { if (grammar_ != nullptr) { const auto &rule = (*grammar_)[start_]; - auto result = rule.parse(s, n, path, log_, error_reporter_); + auto result = rule.parse(s, n, path, log_); return post_process(s, n, result); } return false; @@ -6692,7 +5474,7 @@ public: const char *path = nullptr) const { if (grammar_ != nullptr) { const auto &rule = (*grammar_)[start_]; - auto result = rule.parse(s, n, dt, path, log_, error_reporter_); + auto result = rule.parse(s, n, dt, path, log_); return post_process(s, n, result); } return false; @@ -6703,8 +5485,7 @@ public: const char *path = nullptr) const { if (grammar_ != nullptr) { const auto &rule = (*grammar_)[start_]; - auto result = - rule.parse_and_get_value(s, n, val, path, log_, error_reporter_); + auto result = rule.parse_and_get_value(s, n, val, path, log_); return post_process(s, n, result); } return false; @@ -6715,8 +5496,7 @@ public: const char *path = nullptr) const { if (grammar_ != nullptr) { const auto &rule = (*grammar_)[start_]; - auto result = - rule.parse_and_get_value(s, n, dt, val, path, log_, error_reporter_); + auto result = rule.parse_and_get_value(s, n, dt, val, path, log_); return post_process(s, n, result); } return false; @@ -6833,12 +5613,6 @@ public: void set_logger(Log log) { log_ = log; } - // Receive structured error information instead of (or in addition to) the - // formatted string passed to the logger. - void set_error_reporter(ErrorReporter reporter) { - error_reporter_ = reporter; - } - void set_logger( std::function log) { @@ -6848,20 +5622,14 @@ public: private: bool post_process(const char *s, size_t n, Definition::Result &r) const { - if ((log_ || error_reporter_) && !r.ret) { - r.error_info.output_log(log_, error_reporter_, s, n); - } + if (log_ && !r.ret) { r.error_info.output_log(log_, s, n); } return r.ret && !r.recovered; } std::vector get_no_ast_opt_rules() const { std::vector rules; for (auto &[name, rule] : *grammar_) { - // The optimizer keeps nodes by their emitted name, so honor the - // `ast_name` override when present (else the rule's own name). - if (rule.no_ast_opt) { - rules.push_back(rule.ast_name.empty() ? name : rule.ast_name); - } + if (rule.no_ast_opt) { rules.push_back(name); } } return rules; } @@ -6871,7 +5639,6 @@ private: bool enableLeftRecursion_ = true; bool enablePackratParsing_ = false; Log log_; - ErrorReporter error_reporter_; }; /*-----------------------------------------------------------------------------