diff --git a/src/sage/features/fricas.py b/src/sage/features/fricas.py index 99b9f8c9b5b..b41039a1343 100644 --- a/src/sage/features/fricas.py +++ b/src/sage/features/fricas.py @@ -67,9 +67,12 @@ def is_functional(self): sage: FriCAS().is_functional() # optional - fricas FeatureTestResult('fricas', True) """ - command = ['fricas -nosman -eval ")quit"'] + # Use stdin to pass the quit command instead of -eval because + # GCL-compiled FriCAS misinterprets -eval ")quit" as a Lisp expression. + # See :issue:`40569`. + command = ['fricas', '-nosman'] try: - lines = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True) + lines = subprocess.check_output(command, input=b')quit\n', stderr=subprocess.STDOUT, shell=False) except subprocess.CalledProcessError as e: return FeatureTestResult(self, False, reason="Call `{command}` failed with exit code {e.returncode}".format(command=" ".join(command), e=e)) diff --git a/src/sage/interfaces/fricas.py b/src/sage/interfaces/fricas.py index ed641c3009b..6fe271eda09 100644 --- a/src/sage/interfaces/fricas.py +++ b/src/sage/interfaces/fricas.py @@ -197,6 +197,7 @@ import re import os +import pexpect import sage.interfaces.abc @@ -315,6 +316,7 @@ def __init__(self, name='fricas', command=None, assert max(len(c) for c in FRICAS_INIT_CODE) < eval_using_file_cutoff self.__eval_using_file_cutoff = eval_using_file_cutoff self._COMMANDS_CACHE = '%s/%s_commandlist_cache.sobj' % (DOT_SAGE, name) + self._uses_gcl = None # will be set in _start() # we run the init code in _start to avoid spurious output Expect.__init__(self, name=name, @@ -353,6 +355,28 @@ def _start(self): self.eval(FRICAS_LINENUMBER_OFF_CODE, reformat=False) for line in FRICAS_HELPER_CODE: self.eval(line, reformat=False) + # Detect whether FriCAS is compiled with GCL. + # GCL-based FriCAS may output function declaration messages + # asynchronously after the prompt appears. We need to handle this + # to keep the expect interface synchronized. See :issue:`40569`. + E = self._expect + # First, consume any buffered output from previous commands + # to ensure we get a clean response to the lisp command + while True: + try: + E.expect(self._prompt, timeout=0.1) + except pexpect.TIMEOUT: + break + E.sendline(')lisp (lisp-implementation-type)') + E.expect(self._prompt) + lisp_output = E.before.decode() if isinstance(E.before, bytes) else E.before + self._uses_gcl = 'GCL' in lisp_output + if self._uses_gcl: + for _ in range(3): + try: + E.expect(self._prompt, timeout=0.1) + except pexpect.TIMEOUT: + break # register translations between SymbolicRing and FriCAS Expression self._register_symbols() @@ -896,6 +920,34 @@ def _repr_(self): """ return "FriCAS" + def _eval_line(self, line, allow_use_file=True, wait_for_prompt=True, restart_if_needed=True): + """ + Evaluate a single line of code. + + This method overrides :meth:`sage.interfaces.expect.Expect._eval_line` + to handle FriCAS compiled with GCL, which may output a double prompt + after each command due to buffering issues. + + See :issue:`40569`. + + TESTS:: + + sage: fricas(1+1) # indirect doctest + 2 + """ + result = Expect._eval_line(self, line, allow_use_file=allow_use_file, + wait_for_prompt=wait_for_prompt, + restart_if_needed=restart_if_needed) + # FriCAS compiled with GCL may send an extra prompt after each command. + # We try to consume it to keep the interface synchronized. + # Only do this for GCL-based FriCAS to avoid slowdowns with SBCL. + if self._uses_gcl and wait_for_prompt and self._expect is not None: + try: + self._expect.expect(self._prompt, timeout=0.1) + except pexpect.TIMEOUT: + pass + return result + def __reduce__(self): """ EXAMPLES::