[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
PVS 7.1 introduces several new features and bug fixes. Some of the highlights include an improved installation (PVS is no longer a “tar bomb”), the concept of workspaces, improvements to the XML-RPC interface, some language changes, an improved tracking for TCC changes, improvements to strategy definitions, improvements to theory interpretations, and inclusion of Yices. There are many other changes not mentioned here; it is worth skimming through the rest of this chapter to see which are useful to you.
Installation Notes | ||
New Features | ||
Incompatibilities |
The system is most easily installed by getting a tar file from
http://pvs.csl.sri.com/download.shtml. Untar it in a directory of
your choice (it will create a subdirectory), cd to the subdirectory, and
run sh install.sh
. This sets the path in various scripts, so that
PVS knows where it is. It also byte-compiles the emacs source for your
Emacs. If you move the PVS directory, or change your Emacs, or forgot
whether you ran it already, then simply rerun it.
We strongly suggest getting a pre-built Allegro version, unless you have concerns with the Allegro runtime click-though license, in which case get one of the SBCL Lisp images. It is possible to build from sources, but it can be sensitive to the platform environment. If you decide to try it and run into problems, let us know at pvs-bugs@csl.sri.com. PVS 7.1 is built with Allegro CL 10.0 and SBCL 1.0.47. It is available for Linux 64-bit machines, and Mac 64-bit.
Note that the XML-RPC server is not (yet) available in SBCL.
The latest PVS Allegro supports a new GUI based on Microsoft Visual Studio Code.
To experiment this new GUI, you will need to download VSCode-PVS
(http://github.com/nasa/vscode-pvs), and have
NodeJS (>=12.16.1 https://nodejs.org),
Java JDK (>=1.8, https://openjdk.java.net),
and Visual Studio Code (>=1.32.3, https://code.visualstudio.com).
Up to now PVS has had two notions of context, one which relates to directories (called a PVS context), and the other the usual list of declarations of type theory. The former are now referred to as workspaces, which are just directories with some sort of PVS file in them.
As with directories in a shell, there is a current workspace; other workspaces may be referenced as libraries. Within PVS names, the library is referred to by an identifier, and this is resolved to a workspace in one of three ways, in order:
PVS_LIBRARY_PATH
environment variable.
Note that the second resolution is sensitive to what the current workspace is; in previous PVS versions this was implemented in such a way that the current workspace was treated in a special way, and changing workspaces meant “throwing away” much of the typechecking, etc. results.
In PVS 7, the information associated with a workspace has been encapsulated. This means that the current workspace is not so important. It’s more like the way a shell uses directories; even if you’re in a directory, you can run commands on files in other directories, simply by giving a (relative or absolute) pathname. In the same way, you can now typecheck a file from any workspace, even if it has never been seen by PVS before. It temporarily becomes the current workspace, the command is run, and then the current workspace is restored.
PVS can continue to be used exactly as in earlier versions, but there are several advantages to this change:
The current workspace is in the Lisp global *workspace-session*
.
If the *pvs*
Emacs buffer is not in the prover or ground-evaluator,
you can use (show *workspace-session*)
to see the contents: this is
an instance of the workspace-session
class, with slots
path
: the path associated with this workspace. This must be an
absolute path, as returned by the Common Lisp function truename
.
pvs-files
: this is a hash-table with key the file name string
(without directory or the .pvs
extension), and returns a list. The
first element of the list is the file-date for when it was parsed, and the
rest of the list is the theories that make up the file.
pvs-theories
: this is a hash-table with key the theory id (symbol),
and simply returns the theory instance.
prelude-libs
: this is a list of the prelude extensions allowed by
PVS. See load-prelude-library
for details.
prelude-context
: this is a context used when prelude-libs is set,
as a starting context for typechecking new theories.
lisp-files
: workspaces may have a pvs-lib.lisp
file, that is
automatically loaded when the workspace is referenced as a library.
subdir-alist
: this is a list of subdirs that may be used as library
references from this workspace. Such subdirs must satisfy the rules for a
PVS identifier, in particular, names with hyphens are not directly allowed
(though a library declaration may reference them).
pvs-context
: this is a list that represents the .pvscontext
file.
pvs-context-changed
: this is a flag indicating that the
.pvscontext
has changed, and it will be written at some point.
strat-file-dates
: this is a list of dates associated with the
pvs-strategies
files, first from the PVS directory, then from the
user’s home directory, and finally from the current workspace.
all-subst-mod-params-caches
: this is a cache used by
the subst-mod-params
function, and not interesting unless debugging
that function.
Note that all of these slots were global variables in earlier versions of PVS; this is what is meant by encapsulation.
When PVS is started, it creates a workspace-session instance in
*workspace-session*
, reads the .pvscontext
file, if it
exists, and adds the workspace-session to the
*all-workspace-sessions*
list. When a file is parsed or
typechecked in that workspace, the pvs-files
and
pvs-theories
are updated accordingly.
To parse, typecheck, etc. a PVS file from a different directory, the
macros with-workspace
or with-pvs-file
are used to
temporarily change to the new workspace. If the associated
workspace-session is already in *all-workspace-sessions*
(based on
the path
), then it is simply used, otherwise a new one is created
and added to *all-workspace-sessions*
.
simply takes a path (string or pathname), and temporarily makes that the current workspace and processes the forms returning the value of the last one.
takes a pvsfileref, which in general has the form
dir/file.pvs#theory
, though it can be as simple as a name. It is
pulled apart, and if a dir is included, with-workspace
is
used, and the rest is split on the #
, if it exists. vars is
a list of names, and these are bound in order with the file, theory, and
any others if they are provided (the idea is that there could be a
#formula
or #place
following the #theory
). The
body is then executed with the names in vars bound
accordingly. Note that one must be careful here; if pvsfileref is
just a name, it may be referring to a file or a theory, depending on the
context. Hence there is code such as that for show-tccs
:
(defun show-tccs (theoryref &optional arg) (with-pvs-file (fname thname) theoryref (let* ((theory (get-typechecked-theory (or thname fname))) ...)))) |
Here a theory is expected, and if the theoryref
is just a name,
then fname
is set to it, and thname
is nil
. This is
why the theory
is derived from (or thname fname)
.
IF
can be given as an application, e.g., IF(b,a,c)
.
LAMBDA
expressions may be given a result type, e.g., LAMBDA (x: int)
-> int: f(x)
. This will assign the type [int -> int]
to the
LAMBDA expression, and generate a TCC on f(x)
if necessary.
a < b /= c > 4
. Internally,
this becomes a < b & b /= c & c > 4
. Works for any binary relation
between the same or different types. Note that this will not go inside
parentheses, so (a < b) /= c > 4
will be interpreted as (a <
b) /= c & c > 4
, which probably doesn’t typecheck. (Overloading of
relations or enabling some conversions may allow this to still be
typechecked). Note also that in this case, parentheses are necessary if
what is intended is (a < b) /= (c > 4)
.
TCCs are generated during typechecking for a given declaration, and during
a proof for a given branch. During typechecking, the name of a given TCC
is derived from the declaration, followed by a number; e.g., if the
declaration is named foo
, then the TCCs will be named
foo_TCC1
, etc. Some declarations have many TCCs, with nontrivial
proofs. When such a declaration is modified, it can generate insert,
remove, or otherwise renumber the TCCs, and then associate the wrong
proofs to them.
The new approach keeps track of where the proof came from, creating a
tcc-origin
instance, with the following slots:
root
: the root name for the TCC, before the “_TCC#”.
kind
: one of termination-subtype
, subtype
,
termination
, well-founded
, existence
,
assuming
, mapped-definition-equality
, mapped-axiom
,
cases
, actuals
, disjointness
, or coverage
.
expr
: except for the existence
TCCs, the expanded form
of the expression that was the reason for the TCC.
type
: the type associated; e.g., for subtype
TCCs, the
expected type.
The .prf
file has been extended to include the same information
with TCC proofs. When a PVS file is typechecked, the proofs are read from
the corresponding .prf
. TCC proofs are no longer matched by TCC
name, but instead the kind
and expr
are used. Those that
match exactly are directly used, any remaining proofs are installed (in
order) in the remaining TCCs. This is obviously not perfect, as the
corresponding expr
s may not match, depending on the changes made to
a declaration.
In addition to the above, TCC generation has been cleaned up, making the TCCs more readable.
IF A AND B THEN ...
could
generate a TCC of the form B AND A IMPLIES ...
.
LET
expressions are now treated differently; for example,
LET x = e1, y = e2 IN e
would generate TCCs of the form
FORALL x: x = e1 IMPLIES FORALL y: y = e2 IMPLIES ...
. Now the
form is FORALL x, y: x = e1 AND y = e2 IMPLIES ...
. For large
LET
expressions, this results in much more readable TCCs.
As a result of these changes, many proofs involving TCCs need to be repaired. For shorter proofs, it’s often enough to step through the proof and make the corresponding changes. Sometimes, especially for more complex proofs, it’s not at all obvious how to repair the proof. In this case, the easiest thing is to run the version of PVS where the proof works in parallel with the newer version. In this way it becomes easy to find those steps that diverge, and to fix those.
The PVS GUI is an API for the Prototype Verification System (PVS). In the past, the PVS GUI was based on a modified version of the Emacs Inferior Lisp Mode (http://ilisp.cons.org/) interface. This generally works well, but there are some issues:
For these reasons, we decided to create a new API for a PVS GUI. We have several constraints we want to satisfy:
We started to create an Eclipse plugin for PVS, but found this to be
difficult; there is really nothing in Eclipse to support things like proof
windows, or the various popup buffers that PVS normally does through
Emacs. Note that there is an eclipse
subdirectory in the PVS Git
sources, for anyone who wants to continue this work.
But we took a step back, and started fresh with Visual Studio Code, which so far has proved more flexible, and quicker for prototyping.
The basic architecture consists of a PVS server, with any number of clients. A client can make a request to the PVS URI, and PVS will return a response to that client. In addition, a client can start an XML-RPC server and include that URI with the request, which allows PVS to send requests to the client, e.g., to answer questions, provide file names, or simply get notifications.
In the long run, we expect to make Emacs an XML-RPC client as well, but for now, it uses the same ILISP interface. However, as each JSON method is defined (often based on the corresponding Emacs command), the same JSON will be returned to Emacs. This allows testing at the Emacs level, and provides an incremental way to move toward making Emacs an XML-RPC client.
Although PVS allows any number of clients, there is currently only one main PVS thread. This means that all clients would share the same proof session, etc. This may be useful for collaboration, or for switching between clients (i.e., different GUIs that provide different features). In the future we will explore the possibility of having separate threads associated with different clients, allowing different clients to simultaneously run different proofs, possibly in different contexts.
PVS provides an XML-RPC server when started with a -port
value,
e.g., pvs -port 22334
, normally an unused port between 1024 and 65535.
XML-RPC was chosen because it is supported by most modern languages, and
we chose to implement the JSON-RPC 2.0 protocol within XML-RPC. Directly
using JSON-RPC is possible, but it is not yet widely supported.
There is a single XML-RPC method provided by the PVS server,
pvs.request
, that takes a JSON-RPC request string, and an optional
client URI, which is used to send requests to the client, providing a
2-way communication. Note that PVS does not keep the client URI after
answering the request, thus clients may be killed and restarted at any
time. In like manner, PVS can be restarted without needing
to restart any clients, though it may be necessary to change context,
retypecheck, etc. At the XML-RPC level, the return value includes the
JSON-RPC response, the current PVS context, and the mode (lisp
,
prover
, or evaluator
). Thus if a given client has changed
the context and started a proof, that information is included in the next
request from a different client.
We chose JSON as the data interchange format over XML since it is more compact, and supported by most languages. In addition, there is a JSON Schema available, which we use to describe the API.
Error handling is done as follows. When an XML-RPC request comes in, PVS sets up a condition handler to catch any errors that may happen as a result of processing the request. If the request is badly formed, for a nonexistent method, or if the JSON-RPC request does not include an id, then a response is returned of the form
{"xmlrpc-error": string, "mode": string, "context": string}
If the request is well formed and includes an id, the method is invoked under a new condition handler, and the normal JSON-RPC response is given. This means that errors are returned even if the JSON-RPC request is a notification (without an id). Of course, the client is free to ignore such errors.
There are only a few methods currently supported by PVS; a lot of effort was needed to implement the infrastructure. In particular, the prover was not really designed for a different API, and it was necessary to create hooks for generating a JSON representation of the current goal of a given proof.
The methods currently supported are listed below. Note that details about the possible return values are in the JSON Schema provided with PVS.
This method simply lists the currently available methods.
As described above, PVS may provide information or make requests to the
client. This method lists all the JSON-RPC requests that PVS will invoke
if it is given a URI at the XML-RPC level. Currently it consists of
info
, warning
, debug
, buffer
, yes-no
,
and dialog
. The JSON Schema gives details about the format.
Gives help for any given method returned by list-methods
.
Simply sends a string to be evaluated by the PVS lisp interpreter, and returns a string with the result. Certainly an aid to debugging, but may also be useful for other purposes.
Changes the current context as with the Emacs change-context
command.
Typechecks a specified file. This returns a list of theories, each of which includes the declarations of that theory, as well as their locations.
This is a new method; given a PVS file, it returns an array of PVS identifiers, their location, the associated declaration (as a string), and the file and location where the declaration can be found. This can be used by the client to provide information about a given identifier when the mouse is hovering over that identifier. Clicking on that identifier could bring up the corresponding file and location.
This interrupts any running process, and resets the system to the state where no proof or ground evaluator sessions are running. This may not clear up low-level server/client problems, as those are on a separate thread and more difficult to reset. We’re waiting for a situation where this is an issue.
Given a formula and a theory name, this starts an interactive proof. The result is the current goal consisting of a sequent and other fields, see the JSON schema for details.
This sends the specified proof command to PVS, returning the current goal.
Currently the proof needs to be started with prove-formula
, though in
principle any client (e.g., Emacs) could start the proof and a different
client continue. It’s possible for this to allow collaboration on a
single proof.
As described above, VSCode-PVS is a new PVS GUI based on Visual Studio Code. VSCode-PVS provides several features of modern code editors, including:
The prover has been significantly modified to generate structures suitable
for sending to the GUI. As a means to test this, a new capability was
added to PVS Emacs, making use of the same JSON forms as those
sent to the GUI. By default, PVS uses the old display, simply printing
the sequent in the *pvs*
buffer, displaying the Rule?
prompt, and reading the next prover command.
There are new proof displays available. These are new, and not well tested, please send feedback if you try them out. Keep in mind the distinction Emacs makes between frames, windows, and buffers. A frame is what most systems call a window; each frame can be moved around on the desktop, closed, resized, etc. Frames may be subdivided into windows, and each window displays a buffer. Note that buffers are there, even if they are not currently displayed; there are separate commands for listing buffers, killing buffers, etc.
There are 6 proof display styles available; no-frame
,
0-frame
, 1-frame
, 2-frame
, 3-frame
, and
4-frame
. As you might guess, the names say how many frames are
involved. no-frame
, the default, works as in the past.
The rest create separate windows and frames for different parts of a proof
session: the current goal, the command input, the proof commentary, and
optionally the proof script.
The 0-frame
uses the same frame as the PVS startup frame, and
splits it into separate windows. The 1-frame
creates a new frame
for this purpose. The 2-frame
puts the commentary in a separate
frame, the 3-frame
puts the commentary and proof script in separate
frames, and the 4-frame
puts all four parts in separate frames.
The commentary is used for the running commentary of a proof; information
that is part of the proof session, but not really part of a given proof
step. The command input is currently just a window into the *pvs*
buffer, which still has the proof as before, even when displays are
active. The sequent buffer has the feature that hovering the mouse over
an identifier shows the corresponding declaration; this can be very
helpful in proofs.
The different parts of the proof display have associated faces, and can be
customized. Do M-x customize
and search for pvs
to find all
customizable faces.
A new feature of PVS, developed partly for the new GUI, is the ability to
associate tooltips with each PVS identifier of a PVS file or proof
sequent. These tooltips are only available in typechecked files. They
are automatically available in the GUI after typechecking; in Emacs, run
M-x pvs-add-tooltips
in any typechecked buffer (including the
prelude) and then move the mouse over identifiers in the buffer to see
their types. Clicking middle takes you to the file with the cursor at the
declaration.
The proof command facility has been revamped, primarily in the argument handling. This section is for those who write strategies.
Just to review, strategy definitions such as defstep
have required,
optional, and rest arguments, e.g.,
(defstep foo (a &optional b c &rest d) ...) |
Invocations of foo
require the first argument; if there is a second
argument it is bound to b
, a third argument to c
, and any
remaining arguments are bound to d
. This is similar to Common
Lisp, but in PVS the optional and rest arguments may also be given as
keywords, so foo
could be invoked as either of the equivalent forms
(foo 3 5 7 11 13) (foo 3 5 :d (11 13) :c 7) |
In order to add a new argument to a low-level command, (e.g., the
let-reduce?
flag was added to assert
), then to make this
available to other commands such as grind
meant adding it and the
corresponding documentation to those commands. This is obviously
error-prone. Recently we wanted to add the actuals?
argument of
replace to grind
, in order to allow grind
to work in type
and actual expressions. The problem is that grind
invokes
replace*
, which has a &rest fnums
argument; this does not
allow new arguments to be added without modifying existing proofs.
To solve this immediate problem we added the &key
indicator. It
is similar to the &optional
indicator, but the arguments must be
provided as keywords. Hence replace*
could now be rewritten from
(defstep replace* (&rest fnums) ... |
to
(defstep replace* (&key actuals? &rest fnums) ... |
Existing proofs would not break, but new proofs could invoke
replace*
with an :actuals? t
argument to have
replacement happen inside of types and actuals.
But this only solves part of the problem; propagating this argument to
strategies such as grind
is still error-prone. To deal with this,
we added another indicator: &inherit
. With this, replace*
can be defined as
(defstep replace* (&rest fnums &inherit replace) ...) |
And now replace* automatically inherits all keyword arguments from
replace
. Not only that, but any invocations of replace
within the body of the defstep automatically include keyword invocations
of the replace
call. In effect, where the body was written simply
as (replace y)
, it is replaced in the actual command by
(replace y :dir dir :hide? hide? :actuals? actuals? :dont-delete? dont-delete?) |
Note that this inherited not just the actuals?
argument, but all
the others as well. Note also that if a new argument is added to
replace
, it will be automatically inherited by replace*
.
There is still work to be done; currently optional and key arguments allow
a default, but we want to in addition allow :documentation
and
:kind
keywords, even for required arguments. The documentation
will be used to document the arguments, rather than have them in the main
documentation of the proof command. For optional and key arguments, this
documentation will then propagate, so that, e.g., the documentation for
replace*
directly explains the actuals?
argument, without
having to look up replace
.
The :kind
will be used to support refactoring (among other
possibilities). One problem with refactoring currently is that proofs are
kept as proof scripts, and any types, expressions, etc. are given as
strings. Thus, for example, a command such as (expand "foo")
will
resolve the name foo
, and expand occurrences of it within the
current sequent. This is the case even if foo
is overloaded, and
has three definitions in the sequent. Note that foo
is resolved by
the prover, and the resolutions are used in the subsequent expansions, but
then discarded. If now the user decides that overloaded foo
is
confusing, and wants to name them apart, there is no way to know which
ones to name apart in proof scripts without rerunning them.
The :kind
keyword will be used to associate a kind with each
argument, which in cases such as above would invoke functions that
generate the resolutions and cache the resolution information with the
proof, in a way that it may be used subsequently for refactoring, etc.
The basic idea and motivation are above, the rest of this section goes into more details for those wanting to write new strategies.
The formal arguments list for a new prover command is in a specific order: required, optional, key, rest, and optional. The actual syntax is
prover-args ::= {var}* [&optional {var | (var initform)}*] [&key {var | (var initform)}*] [&rest var] [&inherit {cmd | (cmd :except var+)}*]
Required, optional, and rest arguments work exactly as detailed in the prover guide. Key arguments are similar to optional arguments, but may only be specified by keyword, not by position.
The inherit argument is fundamentally different. A proof command inherits
arguments from other proof commands. This can only be done for proof
commands that are directly referenced in the body; for example,
grind
inherits from replace*
, not replace
, because
it does not directly call the latter. There are two aspects to inheriting
arguments from a command. The first is that the command being defined
takes the union of all the arguments of its own and inherited commands.
The second, is that these inherited arguments are propagated to any calls
of inherited commands.
The inherited arguments are always either optional or key arguments; they
are always treated as key. Hence the order of inherited arguments is not
an issue, though there is a possible issue if the names of arguments clash
with different meanings. This can be controlled to some extent by using
the :except
form, specifying the arguments to be ignored of an
inherited command. If there are more than one unignored arguments with
the same name and different default values, the first is taken as default.
Again, this can easily be controlled, for example, if we have the forms
(defstep foo (x &optional (a 3) &key (b 5) (c 7)) ...) (defstep bar (y &optional (b 7) &key (a 11) (c 13)) ...) (defstep baz (z &key (a 13) &inherit (foo :except c) bar) ...) |
Then baz
gives its own default to a
, and takes foo
s
default for b
and bar
s default for c
.
Propagating the arguments to calls is relatively straightforward. Using
the above as examples, if the body of baz
has an occurrence of
(bar m)
, it is simply replace by (bar m :b b :a a :c c)
and (foo n)
is replaced by (foo n :a a :b b)
.
Note that multiple invocations may be made to, e.g., foo
, and all
of them are replaced. Note also that, e.g., one could be as above, while
the second invocation is (foo :c 31 :a 37)
, which gets expanded to
(foo :c 31 :a 37 :b b)
.
The PVS Emacs command M-x help-pvs-prover-strategy
(C-c C-h
s
) now includes the expanded argument list and definitions, as well as
the original forms. This can be helpful in understanding how the
prover arguments work.
This change has little impact on existing proofs, though in the regression tests it was found that a couple of strategies defined in the NASA libraries were not quite correct, but the old strategy mechanism simply ignored extra arguments. Now those generate an error.
PVS treats positive type parameters specially in datatypes, so that, e.g.,
cons[int](1, null) = cons[nat](1, null)
, but this did not extend
beyond constructors and accessors. Now PVS treats all definitions
accordingly. The basic idea is that if a given definition does not depend
directly on the type, and only on the values, then it is safe to ignore
the type parameter - though typechecking may still generate a TCC.
Thus, for example, length[T]((: 2, 3, 5 :))
is 3, regardless of
which numeric subtype T may be, though unprovable TCCs may result (e.g.,
if T
is even
). Similarly, nth
and every
depend only on the arguments, not on the types. An example of a
definition that depends on the types, not merely the arguments, is
th[T: type from int]: theory ... foo(x: T): int = if (exists (y: T): y > x) then x else 0 endif ... end th |
This change can have an impact on existing proofs, though mostly it makes
them more direct - some proofs involving recursive functions, e.g.,
length[int](x) = length[nat](x)
require convoluted proofs.
The typepred
prover command was extended to include functional
typepreds. Thus if f
has type [D -> {x: R | p(x)}]
, then
the proof command (typepred "f")
would generate a hypothesis of the
form FORALL (x: D): p(f(x))
. Note that some commands such as
skolem
, take a flag that causes typepreds to be generated - this
would also include these functional typepreds.
TCCs that depended on conjunctive forms were generated in some cases in reverse. This has no bearing on soundness or correctness, but some meta-analysis of PVS was made more difficult because of this, so it was fixed.
The yices prover commands have been fully integrated into PVS, and Yices versions 1 and 2 are included in the distribution.
Theory interpretations have been improved in a few ways. First, if an uninterpreted type or constant has declaration formals, they must be included in the mapping, e.g.,
eq [A:TYPE+]: [A -> [A -> bool]] logic_sttfa_pvs : THEORY = logic_sttfa @{@{ connectives_sttfa_th := connectives_sttfa_pvs, eq[T:TYPE+](x:T)(y:T) := x=y @}@}
Second, the context is updated with the theory without mappings when typechecking the RHS. This means that it is not necessary to have two importings in order to specify the RHS, and gives access to declarations of that context that are not being interpreted.
There were a couple of corner cases where TCCs were not being generated. In fixing this, it was decided to rewrite the code to make TCCs easier to understand and work with.
There are three primary sources of incompatibilities with this release. This first is due to more rigorous checking of arguments in proof commands. In the past, if there were left over arguments after pairing command arguments with their invocation, they were simply ignored. Now an error is invoked. Generally these are easy to debug, and they usually indicate a programming error to begin with.
TCC ordering can affect formula numbering (e.g. foo_TCC1
and
foo_TCC2
could be swapped, and within a proof, the branches may be
swapped. In the regression tests, this was fairly rare.
The addition of more typepred information in proofs leads to additional hypotheses, and this can cause formula numbers to be shifted.
In the past, the typechecker was a little strict in creating new variables when a formula change was made (e.g., expanding a definition). It tends to keep the existing variable name more often. Occasionally, this means a reference to, e.g., “i_1” should be changed to “i”.
[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
This document was generated by Sam Owre on September 24, 2020 using texi2html 1.82.