From nobody Sun Sep 28 17:03:46 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=redhat.com ARC-Seal: i=1; a=rsa-sha256; t=1757701434; cv=none; d=zohomail.com; s=zohoarc; b=hYlDHSP3e5f0/b6tfyVW4xokRzjCbgH4pdvwzYZLuhoGe5TRnKd/sFKCVygNK3Co7buJ/DuLbJzUj2tjBZPL97XVrBkI0Di0Y+yujKnQm4VRTsvieRAuMrh49GLLdS8QORr1407SypBoY9d8Pue3iWVWUGbkaQ5Ez8MtFHOSV0E= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1757701434; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=qIORJberizBZ1QFQWihvovMmrYhgEpRz/6jxhp5LxgY=; b=HyAVQhNq9M26Ox7gRJ4MG+dftpBtw2bsYRo235S+KQlFAlBLGxz1XaKxWIWEPauWGz6L375LSqVQVwSK66o3AaqZcW0+eqip3jNwqTiS4XaSdLgQDlLaV39j4UsZRJdIY2wD6HnnW5daIZcR4NwNDo3SpuOEf81vlqNEWKPAoak= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1757701434690296.3848642811081; Fri, 12 Sep 2025 11:23:54 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1ux8Pe-0004Ww-Cu; Fri, 12 Sep 2025 14:22:22 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1ux8Pa-0004VL-5g for qemu-devel@nongnu.org; Fri, 12 Sep 2025 14:22:18 -0400 Received: from us-smtp-delivery-124.mimecast.com ([170.10.129.124]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1ux8PU-0005Fe-ER for qemu-devel@nongnu.org; Fri, 12 Sep 2025 14:22:17 -0400 Received: from mx-prod-mc-08.mail-002.prod.us-west-2.aws.redhat.com (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-693-nYnPnRElOtO3AdMqWAdTIQ-1; Fri, 12 Sep 2025 14:22:07 -0400 Received: from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.93]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-08.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id E9DFA1800359 for ; Fri, 12 Sep 2025 18:22:06 +0000 (UTC) Received: from toolbx.redhat.com (unknown [10.42.28.154]) by mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id E8C4C18003FC; Fri, 12 Sep 2025 18:22:04 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1757701331; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=qIORJberizBZ1QFQWihvovMmrYhgEpRz/6jxhp5LxgY=; b=W9wGaNBDxjgNWTboQ4oxjTc4PduxddjH6uW367WW2Hxb+aSeQvyOccPECOW9P5OoLCfL5I /fk73neDEnFRKChzsIxdfJdFK/89vakdgxaGt038IP7iDHo8Y3Z6+kzLdlzK/YWL8gd9Bm OBit9SHP0prOsZC9A6DbiJm1QSvoQLI= X-MC-Unique: nYnPnRElOtO3AdMqWAdTIQ-1 X-Mimecast-MFC-AGG-ID: nYnPnRElOtO3AdMqWAdTIQ_1757701327 From: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= To: qemu-devel@nongnu.org Cc: Thomas Huth , =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= Subject: [PATCH 1/6] tests/functional: import GDB remote code from avocado Date: Fri, 12 Sep 2025 19:21:55 +0100 Message-ID: <20250912182200.643909-2-berrange@redhat.com> In-Reply-To: <20250912182200.643909-1-berrange@redhat.com> References: <20250912182200.643909-1-berrange@redhat.com> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.93 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=170.10.129.124; envelope-from=berrange@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H5=0.001, RCVD_IN_MSPIKE_WL=0.001, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @redhat.com) X-ZM-MESSAGEID: 1757701437408116600 The gdbmi_parser.py and spark.py modules originated in projects that pre-date avocado and are imported unchanged aside from the 'import' statement in gdbmi_parser.py The gdb.py module is original avocado code that is imported with all classes except GDBRemote removed. Signed-off-by: Daniel P. Berrang=C3=A9 Acked-by: Alex Benn=C3=A9e --- tests/functional/qemu_test/gdb.py | 233 ++++++ tests/functional/qemu_test/gdbmi_parser.py | 419 ++++++++++ tests/functional/qemu_test/spark.py | 850 +++++++++++++++++++++ 3 files changed, 1502 insertions(+) create mode 100644 tests/functional/qemu_test/gdb.py create mode 100644 tests/functional/qemu_test/gdbmi_parser.py create mode 100644 tests/functional/qemu_test/spark.py diff --git a/tests/functional/qemu_test/gdb.py b/tests/functional/qemu_test= /gdb.py new file mode 100644 index 0000000000..913e3b65ab --- /dev/null +++ b/tests/functional/qemu_test/gdb.py @@ -0,0 +1,233 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# A cut-down copy of gdb.py from the avocado project: +# +# Copyright: Red Hat Inc. 2014 +# Authors: Cleber Rosa + +__all__ =3D ["GDB", "GDBServer", "GDBRemote"] + + +import socket + +from . import gdbmi_parser + +#: How the remote protocol signals a transmission success (in ACK mode) +REMOTE_TRANSMISSION_SUCCESS =3D "+" + +#: How the remote protocol signals a transmission failure (in ACK mode) +REMOTE_TRANSMISSION_FAILURE =3D "-" + +#: How the remote protocol flags the start of a packet +REMOTE_PREFIX =3D b"$" + +#: How the remote protocol flags the end of the packet payload, and that t= he +#: two digits checksum follow +REMOTE_DELIMITER =3D b"#" + +#: Rather conservative default maximum packet size for clients using the +#: remote protocol. Individual connections can ask (and do so by default) +#: the server about the maximum packet size they can handle. +REMOTE_MAX_PACKET_SIZE =3D 1024 + + +class UnexpectedResponseError(Exception): + """A response different from the one expected was received from GDB""" + + +class ServerInitTimeoutError(Exception): + """Server took longer than expected to initialize itself properly""" + + +class InvalidPacketError(Exception): + """Packet received has invalid format""" + + +class NotConnectedError(Exception): + """GDBRemote is not connected to a remote GDB server""" + + +class RetransmissionRequestedError(Exception): + """Message integrity was not validated and retransmission is being req= uested""" + + + +class GDBRemote: + """A GDBRemote acts like a client that speaks the GDB remote protocol, + documented at: + + https://sourceware.org/gdb/current/onlinedocs/gdb/Remote-Protocol.html + + Caveat: we currently do not support communicating with devices, only + with TCP sockets. This limitation is basically due to the lack of + use cases that justify an implementation, but not due to any technical + shortcoming. + """ + + def __init__(self, host, port, no_ack_mode=3DTrue, extended_mode=3DTru= e): + """Initializes a new GDBRemote object. + + :param host: the IP address or host name + :type host: str + :param port: the port number where the the remote GDB is listening= on + :type port: int + :param no_ack_mode: if the packet transmission confirmation mode s= hould + be disabled + :type no_ack_mode: bool + :param extended_mode: if the remote extended mode should be enabled + :type param extended_mode: bool + """ + self.host =3D host + self.port =3D port + + # Temporary holder for the class init attributes + self._no_ack_mode =3D no_ack_mode + self.no_ack_mode =3D False + self._extended_mode =3D extended_mode + self.extended_mode =3D False + + self._socket =3D None + + @staticmethod + def checksum(input_message): + """Calculates a remote message checksum. + + More details are available at: + https://sourceware.org/gdb/current/onlinedocs/gdb/Overview.html + + :param input_message: the message input payload, without the + start and end markers + :type input_message: bytes + :returns: two byte checksum + :rtype: bytes + """ + total =3D 0 + for i in input_message: + total +=3D i + result =3D total % 256 + + return b"%02x" % result + + @staticmethod + def encode(data): + """Encodes a command. + + That is, add prefix, suffix and checksum. + + More details are available at: + https://sourceware.org/gdb/current/onlinedocs/gdb/Overview.html + + :param data: the command data payload + :type data: bytes + :returns: the encoded command, ready to be sent to a remote GDB + :rtype: bytes + """ + return b"$%b#%b" % (data, GDBRemote.checksum(data)) + + @staticmethod + def decode(data): + """Decodes a packet and returns its payload. + + More details are available at: + https://sourceware.org/gdb/current/onlinedocs/gdb/Overview.html + + :param data: the command data payload + :type data: bytes + :returns: the encoded command, ready to be sent to a remote GDB + :rtype: bytes + :raises InvalidPacketError: if the packet is not well constructed, + like in checksum mismatches + """ + if data[0:1] !=3D REMOTE_PREFIX: + raise InvalidPacketError + + if data[-3:-2] !=3D REMOTE_DELIMITER: + raise InvalidPacketError + + payload =3D data[1:-3] + checksum =3D data[-2:] + + if payload =3D=3D b"": + expected_checksum =3D b"00" + else: + expected_checksum =3D GDBRemote.checksum(payload) + + if checksum !=3D expected_checksum: + raise InvalidPacketError + + return payload + + def cmd(self, command_data, expected_response=3DNone): + """Sends a command data to a remote gdb server + + Limitations: the current version does not deal with retransmission= s. + + :param command_data: the remote command to send the the remote stub + :type command_data: str + :param expected_response: the (optional) response that is expected + as a response for the command sent + :type expected_response: str + :raises: RetransmissionRequestedError, UnexpectedResponseError + :returns: raw data read from from the remote server + :rtype: str + :raises NotConnectedError: if the socket is not initialized + :raises RetransmissionRequestedError: if there was a failure while + reading the result of the co= mmand + :raises UnexpectedResponseError: if response is unexpected + """ + if self._socket is None: + raise NotConnectedError + + data =3D self.encode(command_data) + self._socket.send(data) + + if not self.no_ack_mode: + transmission_result =3D self._socket.recv(1) + if transmission_result =3D=3D REMOTE_TRANSMISSION_FAILURE: + raise RetransmissionRequestedError + + result =3D self._socket.recv(REMOTE_MAX_PACKET_SIZE) + response_payload =3D self.decode(result) + + if expected_response is not None: + if expected_response !=3D response_payload: + raise UnexpectedResponseError + + return response_payload + + def set_extended_mode(self): + """Enable extended mode. In extended mode, the remote server is ma= de + persistent. The 'R' packet is used to restart the program being + debugged. Original documentation at: + + https://sourceware.org/gdb/current/onlinedocs/gdb/Packets.html#ext= ended-mode + """ + self.cmd(b"!", b"OK") + self.extended_mode =3D True + + def start_no_ack_mode(self): + """Request that the remote stub disable the normal +/- protocol + acknowledgments. Original documentation at: + + https://sourceware.org/gdb/current/onlinedocs/gdb/General-Query-Pa= ckets.html#QStartNoAckMode + """ + self.cmd(b"QStartNoAckMode", b"OK") + self.no_ack_mode =3D True + + def connect(self): + """Connects to the remote target and initializes the chosen modes"= "" + self._socket =3D socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.connect((self.host, self.port)) + + if self._no_ack_mode: + self.start_no_ack_mode() + + if self._extended_mode: + self.set_extended_mode() diff --git a/tests/functional/qemu_test/gdbmi_parser.py b/tests/functional/= qemu_test/gdbmi_parser.py new file mode 100644 index 0000000000..476e824b72 --- /dev/null +++ b/tests/functional/qemu_test/gdbmi_parser.py @@ -0,0 +1,419 @@ +# +# Copyright (c) 2008 Michael Eddington +# +# Permission is hereby granted, free of charge, to any person obtaining a = copy +# of this software and associated documentation files (the "Software"), to= deal +# in the Software without restriction, including without limitation the ri= ghts +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included = in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS = OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL = THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING = FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS I= N THE +# SOFTWARE. +# +# Authors: +# Frank Laub (frank.laub@gmail.com) +# Michael Eddington (mike@phed.org) + + +import pprint +import re + +from . import spark + + +class GdbMiError(Exception): + """ + Exception raised when there is an error parsing GDB/MI output. + """ + + +class Token: + def __init__(self, token_type, value=3DNone): + self.type =3D token_type + self.value =3D value + + def __lt__(self, o): + return self.type < o + + def __gt__(self, o): + return self.type > o + + def __le__(self, o): + return self.type <=3D o + + def __ge__(self, o): + return self.type >=3D o + + def __eq__(self, o): + return self.type =3D=3D o + + def __ne__(self, o): + return self.type !=3D o + + def __repr__(self): + return self.value or self.type + + +class AST: + def __init__(self, ast_type): + self.type =3D ast_type + self._kids =3D [] + + def __getitem__(self, i): + return self._kids[i] + + def __setitem__(self, i, k): + self._kids[i] =3D k + + def __len__(self): + return len(self._kids) + + def __lt__(self, o): + return self.type < o + + def __gt__(self, o): + return self.type > o + + def __le__(self, o): + return self.type <=3D o + + def __ge__(self, o): + return self.type >=3D o + + def __eq__(self, o): + return self.type =3D=3D o + + def __ne__(self, o): + return self.type !=3D o + + +class GdbMiScannerBase(spark.GenericScanner): + def tokenize(self, s): # pylint: disable=3DW0221 + self.rv =3D [] # pylint: disable=3DW0201 + spark.GenericScanner.tokenize(self, s) + return self.rv + + def t_nl(self, s): # pylint: disable=3DW0613 + r"\n|\r\n" + self.rv.append(Token("nl")) + + def t_whitespace(self, s): # pylint: disable=3DW0613 + r"[ \t\f\v]+" + + def t_symbol(self, s): + r",|\{|\}|\[|\]|\=3D" + self.rv.append(Token(s, s)) + + def t_result_type(self, s): + r"\*|\+|\^" + self.rv.append(Token("result_type", s)) + + def t_stream_type(self, s): + r"\@|\&|\~" + self.rv.append(Token("stream_type", s)) + + def t_string(self, s): + r"[\w-]+" + self.rv.append(Token("string", s)) + + def t_c_string(self, s): + r"\".*?(? 2: + print(f"{tokens[i - 3]} {tokens[i - 2]} " f"{tokens[i - 1]} {t= okens[i]}") + raise GdbMiError(f"Syntax error at or near {int(i)}:'{token}' toke= n") + + +class GdbMiInterpreter(spark.GenericASTTraversal): + def __init__(self, ast): + spark.GenericASTTraversal.__init__(self, ast) + self.postorder() + + @staticmethod + def __translate_type(token_type): + table =3D { + "^": "result", + "=3D": "notify", + "+": "status", + "*": "exec", + "~": "console", + "@": "target", + "&": "log", + } + return table[token_type] + + @staticmethod + def n_result(node): + # result ::=3D variable =3D value + node.value =3D {node[0].value: node[2].value} + # print 'result: %s' % node.value + + @staticmethod + def n_tuple(node): + if len(node) =3D=3D 2: + # tuple ::=3D {} + node.value =3D {} + elif len(node) =3D=3D 3: + # tuple ::=3D { result } + node.value =3D node[1].value + elif len(node) =3D=3D 4: + # tuple ::=3D { result result_list } + node.value =3D node[1].value + for result in node[2].value: + for n, v in list(result.items()): + if n in node.value: + # print '**********list conversion: [%s] %s -> %s'= % (n, node.value[n], v) + old =3D node.value[n] + if not isinstance(old, list): + node.value[n] =3D [node.value[n]] + node.value[n].append(v) + else: + node.value[n] =3D v + else: + raise GdbMiError("Invalid tuple") + # print 'tuple: %s' % node.value + + @staticmethod + def n_list(node): + if len(node) =3D=3D 2: + # list ::=3D [] + node.value =3D [] + elif len(node) =3D=3D 3: + # list ::=3D [ value ] + node.value =3D [node[1].value] + elif len(node) =3D=3D 4: + # list ::=3D [ value value_list ] + node.value =3D [node[1].value] + node[2].value + # list ::=3D [ result ] + # list ::=3D [ result result_list ] + # list ::=3D { value } + # list ::=3D { value value_list } + # print 'list %s' % node.value + + @staticmethod + def n_value_list(node): + if len(node) =3D=3D 2: + # value_list ::=3D , value + node.value =3D [node[1].value] + elif len(node) =3D=3D 3: + # value_list ::=3D , value value_list + node.value =3D [node[1].value] + node[2].value + + @staticmethod + def n_result_list(node): + if len(node) =3D=3D 2: + # result_list ::=3D , result + node.value =3D [node[1].value] + else: + # result_list ::=3D , result result_list + node.value =3D [node[1].value] + node[2].value + # print 'result_list: %s' % node.value + + @staticmethod + def n_result_record(node): + node.value =3D node[0].value + if len(node) =3D=3D 3: + # result_record ::=3D result_header result_list nl + node.value["results"] =3D node[1].value + elif len(node) =3D=3D 2: + # result_record ::=3D result_header nl + pass + # print 'result_record: %s' % (node.value) + + def n_result_header(self, node): + if len(node) =3D=3D 3: + # result_header ::=3D token result_type class + node.value =3D { + "token": node[0].value, + "type": self.__translate_type(node[1].value), + "class_": node[2].value, + "record_type": "result", + } + elif len(node) =3D=3D 2: + # result_header ::=3D result_type class + node.value =3D { + "token": None, + "type": self.__translate_type(node[0].value), + "class_": node[1].value, + "record_type": "result", + } + + def n_stream_record(self, node): + # stream_record ::=3D stream_type c_string nl + node.value =3D { + "type": self.__translate_type(node[0].value), + "value": node[1].value, + "record_type": "stream", + } + # print 'stream_record: %s' % node.value + + @staticmethod + def n_record_list(node): + if len(node) =3D=3D 1: + # record_list ::=3D generic_record + node.value =3D [node[0].value] + elif len(node) =3D=3D 2: + # record_list ::=3D generic_record record_list + node.value =3D [node[0].value] + node[1].value + # print 'record_list: %s' % node.value + + # def default(self, node): + # print 'default: ' + node.type + + +class GdbDynamicObject: + def __init__(self, dict_): + self.graft(dict_) + + def __repr__(self): + return pprint.pformat(self.__dict__) + + def __bool__(self): + return len(self.__dict__) > 0 + + def __getitem__(self, i): + if not i and len(self.__dict__) > 0: + return self + raise IndexError + + def __getattr__(self, name): + if name.startswith("__"): + raise AttributeError + + def graft(self, dict_): + for name, value in list(dict_.items()): + name =3D name.replace("-", "_") + if isinstance(value, dict): + value =3D GdbDynamicObject(value) + elif isinstance(value, list): + x =3D value + value =3D [] + for item in x: + if isinstance(item, dict): + item =3D GdbDynamicObject(item) + value.append(item) + setattr(self, name, value) + + +class GdbMiRecord: + def __init__(self, record): + self.result =3D None + for name, value in list(record[0].items()): + name =3D name.replace("-", "_") + if name =3D=3D "results": + for result in value: + if not self.result: + self.result =3D GdbDynamicObject(result) + else: + # graft this result to self.results + self.result.graft(result) + else: + setattr(self, name, value) + + def __repr__(self): + return pprint.pformat(self.__dict__) + + +class session: # pylint: disable=3DC0103 + def __init__(self): + self.the_scanner =3D GdbMiScanner() + self.the_parser =3D GdbMiParser() + self.the_interpreter =3D GdbMiInterpreter + self.the_output =3D GdbMiRecord + + def scan(self, data_input): + return self.the_scanner.tokenize(data_input) + + def parse(self, tokens): + return self.the_parser.parse(tokens) + + def process(self, data_input): + tokens =3D self.scan(data_input) + ast =3D self.parse(tokens) + self.the_interpreter(ast) + return self.the_output(ast.value) diff --git a/tests/functional/qemu_test/spark.py b/tests/functional/qemu_te= st/spark.py new file mode 100644 index 0000000000..1f98b9508c --- /dev/null +++ b/tests/functional/qemu_test/spark.py @@ -0,0 +1,850 @@ +# Copyright (c) 1998-2002 John Aycock +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +__version__ =3D "SPARK-0.7 (pre-alpha-7)" + +import re + + +def _namelist(instance): + namelist, namedict, classlist =3D [], {}, [instance.__class__] + for c in classlist: + for b in c.__bases__: + classlist.append(b) # pylint: disable=3DW4701 + for name in c.__dict__.keys(): + if name not in namedict: + namelist.append(name) + namedict[name] =3D 1 + return namelist + + +class GenericScanner: + def __init__(self, flags=3D0): + pattern =3D self.reflect() + self.re =3D re.compile(pattern, re.VERBOSE | flags) + + self.index2func =3D {} + for name, number in self.re.groupindex.items(): + self.index2func[number - 1] =3D getattr(self, "t_" + name) + + def makeRE(self, name): # pylint: disable=3DC0103 + doc =3D getattr(self, name).__doc__ + rv =3D f"(?P<{name[2:]}>{doc})" + return rv + + def reflect(self): + rv =3D [] + for name in _namelist(self): + if name[:2] =3D=3D "t_" and name !=3D "t_default": + rv.append(self.makeRE(name)) + + rv.append(self.makeRE("t_default")) + return "|".join(rv) + + @staticmethod + def error(s, pos): # pylint: disable=3DW0613 + print(f"Lexical error at position {pos}") + raise SystemExit + + def tokenize(self, s): + pos =3D 0 + n =3D len(s) + while pos < n: + m =3D self.re.match(s, pos) + if m is None: + self.error(s, pos) + + groups =3D m.groups() + for i, group in enumerate(groups): + if group and i in self.index2func: + self.index2func[i](group) + pos =3D m.end() + + @staticmethod + def t_default(s): # pylint: disable=3DW0613 + r"( . | \n )+" + print("Specification error: unmatched input") + raise SystemExit + + +# +# Extracted from GenericParser and made global so that [un]picking works. +# + + +class _State: + def __init__(self, stateno, items): + self.t, self.complete, self.items =3D [], [], items + self.stateno =3D stateno + + +# pylint: disable=3DR0902,R0904 +class GenericParser: + # + # An Earley parser, as per J. Earley, "An Efficient Context-Free + # Parsing Algorithm", CACM 13(2), pp. 94-102. Also J. C. Earley, + # "An Efficient Context-Free Parsing Algorithm", Ph.D. thesis, + # Carnegie-Mellon University, August 1968. New formulation of + # the parser according to J. Aycock, "Practical Earley Parsing + # and the SPARK Toolkit", Ph.D. thesis, University of Victoria, + # 2001, and J. Aycock and R. N. Horspool, "Practical Earley + # Parsing", unpublished paper, 2001. + # + + def __init__(self, start): + self.rules =3D {} + self.rule2func =3D {} + self.rule2name =3D {} + self.collectRules() + self.augment(start) + self.ruleschanged =3D 1 + + _NULLABLE =3D r"\e_" + _START =3D "START" + _BOF =3D "|-" + + # + # When pickling, take the time to generate the full state machine; + # some information is then extraneous, too. Unfortunately we + # can't save the rule2func map. + # + def __getstate__(self): + if self.ruleschanged: + # + # FIX ME - duplicated from parse() + # + self.computeNull() + self.newrules =3D {} # pylint: disable=3DW0201 + self.new2old =3D {} # pylint: disable=3DW0201 + self.makeNewRules() + self.ruleschanged =3D 0 + self.edges, self.cores =3D {}, {} # pylint: disable=3DW0201 + self.states =3D {0: self.makeState0()} # pylint: disable=3DW0= 201 + self.makeState(0, self._BOF) + # + # FIX ME - should find a better way to do this.. + # + changes =3D 1 + while changes: + changes =3D 0 + for k, v in self.edges.items(): + if v is None: + state, sym =3D k + if state in self.states: + self.goto(state, sym) + changes =3D 1 + rv =3D self.__dict__.copy() + for s in self.states.values(): + del s.items + del rv["rule2func"] + del rv["nullable"] + del rv["cores"] + return rv + + def __setstate__(self, d): + self.rules =3D {} + self.rule2func =3D {} + self.rule2name =3D {} + self.collectRules() + start =3D d["rules"][self._START][0][1][1] # Blech. + self.augment(start) + d["rule2func"] =3D self.rule2func + d["makeSet"] =3D self.makeSet_fast + self.__dict__ =3D d # pylint: disable=3DW0201 + + # + # A hook for GenericASTBuilder and GenericASTMatcher. Mess + # thee not with this; nor shall thee toucheth the _preprocess + # argument to addRule. + # + @staticmethod + def preprocess(rule, func): + return rule, func + + def addRule(self, doc, func, _preprocess=3D1): # pylint: disable=3DC0= 103 + fn =3D func + rules =3D doc.split() + + index =3D [] + for i, rule in enumerate(rules): + if rule =3D=3D "::=3D": + index.append(i - 1) + index.append(len(rules)) + + for i in range(len(index) - 1): + lhs =3D rules[index[i]] + rhs =3D rules[index[i] + 2 : index[i + 1]] + rule =3D (lhs, tuple(rhs)) + + if _preprocess: + rule, fn =3D self.preprocess(rule, func) + + if lhs in self.rules: + self.rules[lhs].append(rule) + else: + self.rules[lhs] =3D [rule] + self.rule2func[rule] =3D fn + self.rule2name[rule] =3D func.__name__[2:] + self.ruleschanged =3D 1 + + def collectRules(self): # pylint: disable=3DC0103 + for name in _namelist(self): + if name[:2] =3D=3D "p_": + func =3D getattr(self, name) + doc =3D func.__doc__ + self.addRule(doc, func) + + def augment(self, start): + rule =3D f"{self._START} ::=3D {self._BOF} {start}" + self.addRule(rule, lambda args: args[1], 0) + + def computeNull(self): # pylint: disable=3DC0103 + self.nullable =3D {} # pylint: disable=3DW0201 + tbd =3D [] + + for rulelist in self.rules.values(): + lhs =3D rulelist[0][0] + self.nullable[lhs] =3D 0 + for rule in rulelist: + rhs =3D rule[1] + if not rhs: + self.nullable[lhs] =3D 1 + continue + # + # We only need to consider rules which + # consist entirely of nonterminal symbols. + # This should be a savings on typical + # grammars. + # + for sym in rhs: + if sym not in self.rules: + break + else: + tbd.append(rule) + changes =3D 1 + while changes: + changes =3D 0 + for lhs, rhs in tbd: + if self.nullable[lhs]: + continue + for sym in rhs: + if not self.nullable[sym]: + break + else: + self.nullable[lhs] =3D 1 + changes =3D 1 + + def makeState0(self): # pylint: disable=3DC0103 + s0 =3D _State(0, []) + for rule in self.newrules[self._START]: + s0.items.append((rule, 0)) + return s0 + + def finalState(self, tokens): # pylint: disable=3DC0103 + # + # Yuck. + # + if len(self.newrules[self._START]) =3D=3D 2 and not tokens: + return 1 + start =3D self.rules[self._START][0][1][1] + return self.goto(1, start) + + def makeNewRules(self): # pylint: disable=3DC0103 + worklist =3D [] + for rulelist in self.rules.values(): + for rule in rulelist: + worklist.append((rule, 0, 1, rule)) + + for rule, i, candidate, oldrule in worklist: + lhs, rhs =3D rule + n =3D len(rhs) + while i < n: + sym =3D rhs[i] + if sym not in self.rules or not self.nullable[sym]: + candidate =3D 0 + i +=3D 1 + continue + + newrhs =3D list(rhs) + newrhs[i] =3D self._NULLABLE + sym + newrule =3D (lhs, tuple(newrhs)) + # pylint: disable=3DW4701 + worklist.append((newrule, i + 1, candidate, oldrule)) + candidate =3D 0 + i +=3D 1 + else: # pylint: disable=3DW0120 + if candidate: + lhs =3D self._NULLABLE + lhs + rule =3D (lhs, rhs) + if lhs in self.newrules: + self.newrules[lhs].append(rule) + else: + self.newrules[lhs] =3D [rule] + self.new2old[rule] =3D oldrule + + @staticmethod + def typestring(token): # pylint: disable=3DW0613 + return None + + @staticmethod + def error(token): + print(f"Syntax error at or near `{token}' token") + raise SystemExit + + def parse(self, tokens): + sets =3D [[(1, 0), (2, 0)]] + self.links =3D {} # pylint: disable=3DW0201 + + if self.ruleschanged: + self.computeNull() + self.newrules =3D {} # pylint: disable=3DW0201 + self.new2old =3D {} # pylint: disable=3DW0201 + self.makeNewRules() + self.ruleschanged =3D 0 + self.edges, self.cores =3D {}, {} # pylint: disable=3DW0201 + self.states =3D {0: self.makeState0()} # pylint: disable=3DW0= 201 + self.makeState(0, self._BOF) + + for i, token in enumerate(tokens): + sets.append([]) + + if sets[i] =3D=3D []: + break + self.makeSet(token, sets, i) + else: + sets.append([]) + self.makeSet(None, sets, len(tokens)) + + finalitem =3D (self.finalState(tokens), 0) + if finalitem not in sets[-2]: + if len(tokens) > 0: + self.error(tokens[i - 1]) # pylint: disable=3DW0631 + else: + self.error(None) + + return self.buildTree(self._START, finalitem, tokens, len(sets) - = 2) + + def isnullable(self, sym): + # + # For symbols in G_e only. If we weren't supporting 1.5, + # could just use sym.startswith(). + # + return self._NULLABLE =3D=3D sym[0 : len(self._NULLABLE)] + + def skip(self, hs, pos=3D0): + n =3D len(hs[1]) + while pos < n: + if not self.isnullable(hs[1][pos]): + break + pos +=3D 1 + return pos + + def makeState(self, state, sym): # pylint: disable=3DR0914, R0912, C0= 103 + assert sym is not None + # + # Compute \epsilon-kernel state's core and see if + # it exists already. + # + kitems =3D [] + for rule, pos in self.states[state].items: + _, rhs =3D rule + if rhs[pos : pos + 1] =3D=3D (sym,): + kitems.append((rule, self.skip(rule, pos + 1))) + core =3D kitems + + core.sort() + tcore =3D tuple(core) + if tcore in self.cores: + return self.cores[tcore] + # + # Nope, doesn't exist. Compute it and the associated + # \epsilon-nonkernel state together; we'll need it right away. + # + k =3D self.cores[tcore] =3D len(self.states) + ks, nk =3D _State(k, kitems), _State(k + 1, []) + self.states[k] =3D ks + predicted =3D {} + + edges =3D self.edges + rules =3D self.newrules + for x in ks, nk: + worklist =3D x.items + for item in worklist: + rule, pos =3D item + _, rhs =3D rule + if pos =3D=3D len(rhs): + x.complete.append(rule) + continue + + next_sym =3D rhs[pos] + key =3D (x.stateno, next_sym) + if next_sym not in rules: + if key not in edges: + edges[key] =3D None + x.t.append(next_sym) + else: + edges[key] =3D None + if next_sym not in predicted: + predicted[next_sym] =3D 1 + for prule in rules[next_sym]: + ppos =3D self.skip(prule) + new =3D (prule, ppos) + nk.items.append(new) + # + # Problem: we know K needs generating, but we + # don't yet know about NK. Can't commit anything + # regarding NK to self.edges until we're sure. Should + # we delay committing on both K and NK to avoid this + # hacky code? This creates other problems.. + # + if x is ks: + edges =3D {} + + if nk.items =3D=3D []: + return k + + # + # Check for \epsilon-nonkernel's core. Unfortunately we + # need to know the entire set of predicted nonterminals + # to do this without accidentally duplicating states. + # + core =3D sorted(predicted.keys()) + tcore =3D tuple(core) + if tcore in self.cores: + self.edges[(k, None)] =3D self.cores[tcore] + return k + + nk =3D self.cores[tcore] =3D self.edges[(k, None)] =3D nk.stateno + self.edges.update(edges) + self.states[nk] =3D nk + return k + + def goto(self, state, sym): + key =3D (state, sym) + if key not in self.edges: + # + # No transitions from state on sym. + # + return None + + rv =3D self.edges[key] + if rv is None: + # + # Target state isn't generated yet. Remedy this. + # + rv =3D self.makeState(state, sym) + self.edges[key] =3D rv + return rv + + def gotoT(self, state, t): # pylint: disable=3DC0103 + return [self.goto(state, t)] + + def gotoST(self, state, st): # pylint: disable=3DC0103 + rv =3D [] + for t in self.states[state].t: + if st =3D=3D t: + rv.append(self.goto(state, t)) + return rv + + # pylint: disable=3DR0913 + def add(self, input_set, item, i=3DNone, predecessor=3DNone, causal=3D= None): + if predecessor is None: + if item not in input_set: + input_set.append(item) + else: + key =3D (item, i) + if item not in input_set: + self.links[key] =3D [] + input_set.append(item) + self.links[key].append((predecessor, causal)) + + def makeSet(self, token, sets, i): # pylint: disable=3DR0914,C0103 + cur, next_item =3D sets[i], sets[i + 1] + + ttype =3D ( # pylint: disable=3DR1709 + token is not None and self.typestring(token) or None + ) + if ttype is not None: + fn, arg =3D self.gotoT, ttype + else: + fn, arg =3D self.gotoST, token + + for item in cur: + ptr =3D (item, i) + state, parent =3D item + add =3D fn(state, arg) + for k in add: + if k is not None: + self.add(next_item, (k, parent), i + 1, ptr) + nk =3D self.goto(k, None) + if nk is not None: + self.add(next_item, (nk, i + 1)) + + if parent =3D=3D i: + continue + + for rule in self.states[state].complete: + lhs, _ =3D rule + for pitem in sets[parent]: + pstate, pparent =3D pitem + k =3D self.goto(pstate, lhs) + if k is not None: + why =3D (item, i, rule) + pptr =3D (pitem, parent) + self.add(cur, (k, pparent), i, pptr, why) + nk =3D self.goto(k, None) + if nk is not None: + self.add(cur, (nk, i)) + + def makeSet_fast(self, token, sets, i): # pylint: disable=3DR0914, R0= 912, C0103 + # + # Call *only* when the entire state machine has been built! + # It relies on self.edges being filled in completely, and + # then duplicates and inlines code to boost speed at the + # cost of extreme ugliness. + # + cur, next_item =3D sets[i], sets[i + 1] + ttype =3D ( # pylint: disable=3DR1709 + token is not None and self.typestring(token) or None + ) + + for item in cur: # pylint: disable=3DR1702 + ptr =3D (item, i) + state, parent =3D item + if ttype is not None: + k =3D self.edges.get((state, ttype), None) + if k is not None: + # self.add(next_item, (k, parent), i + 1, ptr) + # INLINED --v + new =3D (k, parent) + key =3D (new, i + 1) + if new not in next_item: + self.links[key] =3D [] + next_item.append(new) + self.links[key].append((ptr, None)) + # INLINED --^ + # nk =3D self.goto(k, None) + nk =3D self.edges.get((k, None), None) + if nk is not None: + # self.add(next_item, (nk, i + 1)) + # INLINED --v + new =3D (nk, i + 1) + if new not in next_item: + next_item.append(new) + # INLINED --^ + else: + add =3D self.gotoST(state, token) + for k in add: + if k is not None: + self.add(next_item, (k, parent), i + 1, ptr) + # nk =3D self.goto(k, None) + nk =3D self.edges.get((k, None), None) + if nk is not None: + self.add(next_item, (nk, i + 1)) + + if parent =3D=3D i: + continue + + for rule in self.states[state].complete: + lhs, _ =3D rule + for pitem in sets[parent]: + pstate, pparent =3D pitem + # k =3D self.goto(pstate, lhs) + k =3D self.edges.get((pstate, lhs), None) + if k is not None: + why =3D (item, i, rule) + pptr =3D (pitem, parent) + # self.add(cur, (k, pparent), + # i, pptr, why) + # INLINED --v + new =3D (k, pparent) + key =3D (new, i) + if new not in cur: + self.links[key] =3D [] + cur.append(new) + self.links[key].append((pptr, why)) + # INLINED --^ + # nk =3D self.goto(k, None) + nk =3D self.edges.get((k, None), None) + if nk is not None: + # self.add(cur, (nk, i)) + # INLINED --v + new =3D (nk, i) + if new not in cur: + cur.append(new) + # INLINED --^ + + def predecessor(self, key, causal): + for p, c in self.links[key]: + if c =3D=3D causal: + return p + assert 0 + + def causal(self, key): + links =3D self.links[key] + if len(links) =3D=3D 1: + return links[0][1] + choices =3D [] + rule2cause =3D {} + for _, c in links: + rule =3D c[2] + choices.append(rule) + rule2cause[rule] =3D c + return rule2cause[self.ambiguity(choices)] + + def deriveEpsilon(self, nt): # pylint: disable=3DC0103 + if len(self.newrules[nt]) > 1: + rule =3D self.ambiguity(self.newrules[nt]) + else: + rule =3D self.newrules[nt][0] + + rhs =3D rule[1] + attr =3D [None] * len(rhs) + + for i in range(len(rhs) - 1, -1, -1): + attr[i] =3D self.deriveEpsilon(rhs[i]) + return self.rule2func[self.new2old[rule]](attr) + + def buildTree(self, nt, item, tokens, k): # pylint: disable=3DC0103 + state, _ =3D item + + choices =3D [] + for rule in self.states[state].complete: + if rule[0] =3D=3D nt: + choices.append(rule) + rule =3D choices[0] + if len(choices) > 1: + rule =3D self.ambiguity(choices) + + rhs =3D rule[1] + attr =3D [None] * len(rhs) + + for i in range(len(rhs) - 1, -1, -1): + sym =3D rhs[i] + if sym not in self.newrules: + if sym !=3D self._BOF: + attr[i] =3D tokens[k - 1] + key =3D (item, k) + item, k =3D self.predecessor(key, None) + # elif self.isnullable(sym): + elif self._NULLABLE =3D=3D sym[0 : len(self._NULLABLE)]: + attr[i] =3D self.deriveEpsilon(sym) + else: + key =3D (item, k) + why =3D self.causal(key) + attr[i] =3D self.buildTree(sym, why[0], tokens, why[1]) + item, k =3D self.predecessor(key, why) + return self.rule2func[self.new2old[rule]](attr) + + def ambiguity(self, rules): + # + # FIX ME - problem here and in collectRules() if the same rule + # appears in >1 method. Also undefined results if rules + # causing the ambiguity appear in the same method. + # + sortlist =3D [] + name2index =3D {} + for i, rule in enumerate(rules): + _, rhs =3D rule =3D rule + name =3D self.rule2name[self.new2old[rule]] + sortlist.append((len(rhs), name)) + name2index[name] =3D i + sortlist.sort() + result_list =3D [name for _, name in sortlist] + return rules[name2index[self.resolve(result_list)]] + + @staticmethod + def resolve(input_list): + # + # Resolve ambiguity in favor of the shortest RHS. + # Since we walk the tree from the top down, this + # should effectively resolve in favor of a "shift". + # + return input_list[0] + + +# +# GenericASTBuilder automagically constructs a concrete/abstract syntax t= ree +# for a given input. The extra argument is a class (not an instance!) +# which supports the "__setslice__" and "__len__" methods. +# +# FIX ME - silently overrides any user code in methods. +# + + +class GenericASTBuilder(GenericParser): + def __init__(self, AST, start): + GenericParser.__init__(self, start) + self.ast =3D AST + + def preprocess(self, rule, func): # pylint: disable=3DW0221 + # pylint: disable=3DC3001 + rebind =3D ( + lambda lhs, self=3Dself: lambda args, lhs=3Dlhs, self=3Dself: = self.buildASTNode( + args, lhs + ) + ) + lhs, _ =3D rule + return rule, rebind(lhs) + + def buildASTNode(self, args, lhs): # pylint: disable=3DC0103 + children =3D [] + for arg in args: + if isinstance(arg, self.ast): + children.append(arg) + else: + children.append(self.terminal(arg)) + return self.nonterminal(lhs, children) + + @staticmethod + def terminal(token): + return token + + def nonterminal(self, token_type, args): + rv =3D self.ast(token_type) + rv[: len(args)] =3D args + return rv + + +# +# GenericASTTraversal is a Visitor pattern according to Design Patterns. = For +# each node it attempts to invoke the method n_, falling +# back onto the default() method if the n_* can't be found. The preorder +# traversal also looks for an exit hook named n__exit (no defa= ult +# routine is called if it's not found). To prematurely halt traversal +# of a subtree, call the prune() method -- this only makes sense for a +# preorder traversal. Node type is determined via the typestring() metho= d. +# + + +class GenericASTTraversalPruningException(Exception): + pass + + +class GenericASTTraversal: + def __init__(self, ast): + self.ast =3D ast + + @staticmethod + def typestring(node): + return node.type + + @staticmethod + def prune(): + raise GenericASTTraversalPruningException + + def preorder(self, node=3DNone): + if node is None: + node =3D self.ast + + try: + name =3D "n_" + self.typestring(node) + if hasattr(self, name): + func =3D getattr(self, name) + func(node) + else: + self.default(node) + except GenericASTTraversalPruningException: + return + + for kid in node: + self.preorder(kid) + + name =3D name + "_exit" + if hasattr(self, name): + func =3D getattr(self, name) + func(node) + + def postorder(self, node=3DNone): + if node is None: + node =3D self.ast + + for kid in node: + self.postorder(kid) + + name =3D "n_" + self.typestring(node) + if hasattr(self, name): + func =3D getattr(self, name) + func(node) + else: + self.default(node) + + def default(self, node): + pass + + +# +# GenericASTMatcher. AST nodes must have "__getitem__" and "__cmp__" +# implemented. +# +# FIX ME - makes assumptions about how GenericParser walks the parse tree. +# + + +class GenericASTMatcher(GenericParser): + def __init__(self, start, ast): + GenericParser.__init__(self, start) + self.ast =3D ast + + def preprocess(self, rule, func): # pylint: disable=3DW0221 + # pylint: disable=3DC3001 + rebind =3D ( + lambda func, self=3Dself: lambda args, func=3Dfunc, self=3Dsel= f: self.foundMatch( + args, func + ) + ) + lhs, rhs =3D rule + rhslist =3D list(rhs) + rhslist.reverse() + + return (lhs, tuple(rhslist)), rebind(func) + + @staticmethod + def foundMatch(args, func): # pylint: disable=3DC0103 + func(args[-1]) + return args[-1] + + def match_r(self, node): + self.input.insert(0, node) + children =3D 0 + + for child in node: + if not children: + self.input.insert(0, "(") + children +=3D 1 + self.match_r(child) + + if children > 0: + self.input.insert(0, ")") + + def match(self, ast=3DNone): + if ast is None: + ast =3D self.ast + self.input =3D [] # pylint: disable=3DW0201 + + self.match_r(ast) + self.parse(self.input) + + def resolve(self, input_list): # pylint: disable=3DW0221 + # + # Resolve ambiguity in favor of the longest RHS. + # + return input_list[-1] --=20 2.50.1