Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/sage/features/fricas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
52 changes: 52 additions & 0 deletions src/sage/interfaces/fricas.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@

import re
import os
import pexpect

import sage.interfaces.abc

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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::
Expand Down
Loading