What is scoping?
The Lurch Deductive Engine supports the common notion of nested scopes for symbols, a notion that appears in various ways in both mathematical language and computer programming languages.
- In mathematics, when we begin a proof by saying, "Let $x$ be an arbitrary real number," we do not expect that once the proof is over, we are still going to talk about $x$. There is a notion of what computer scientists call "variable scope" that would say that the symbol $x$ is no longer accessible, or "in scope," after the proof has closed.
- In almost all programming languages, this same principle appears. For
example, in JavaScript, the language in which the LDE is developed, one
can write
function ( x ) { ... }
and thex
is defined only within the curly brackets. That is its "scope."
Scoping in Lurch
In Lurch, we use Environments to delimit scope. But how
do we know where a symbol's scope begins? Notice in the two examples above
that explicit syntax marks the beginning of a variable's scope (in
mathematics, "Let...be arbitrary" and in JavaScript function (...)
). In
Lurch, we have three methods for marking the beginning of a variable's scope,
each of which could be used to represent mathematical content like the
example above.
- A Declaration of a symbol marks the beginning of a
scope for that symbol, and includes the declaration's body (if it has
one) and any other LogicConcepts that follow the
declaration in its parent environment.
- We say that symbols declared in this way are explicitly declared.
- You can find out which symbols a Declaration declares with its symbols() function.
- A BindingEnvironment that binds a symbol $x$
is itself the scope for $x$; that is, the scope lasts from the first
through the last children of the environment, inclusive.
- Symbols declared in this way are also said to be explicitly declared.
- You can find out which symbols a BindingEnvironment declares with its boundSymbolNames() function.
- A BindingExpression functions just like a
BindingEnvironment, except within one single
expression. These are used for quantification, indexed operators,
dummy variables in integration and $\lambda$ notation, and more.
- Symbols introduced in this way are said to be bound.
- You can find out which symbols a BindingExpression declares with its boundSymbolNames() function.
- Any LogicConcept can be marked as implicitly
declaring a symbol. If the LogicConcept so marked
is an Environment, then the implicit declaration
functions just like case 2, above, and otherwise, it functions like case
1.
- Naturally, we say that a symbol declared in this way is implicitly declared.
- You can add implicit declarations to any LogicConcept with the addImplicitDeclaration() function in this module, but you should do so only during the appropriate phase of scope checking. Refer to the documentation of validate() for details.
- You can find out which symbols a LogicConcept implicitly declares with this module's implicitDeclarations() function.
The fourth case may seem redundant, since it just repeats the functionality of the earlier cases, but we will use that case to represent in a natural way those mathematical situations in which it is typical to begin using symbols without declaring them, such as in an algebra exercise (where almost nobody says "let $x$ be arbitrary" before they solve for $x$), or a derivation in propositional logic (where almost nobody says "let $P$ and $Q$ be arbitrary propositions" before they write a propositional proof). There are many other cases where symbols are used without being introduced, but these are just two common examples.
Tools in this module
This module provides two types of functionality. First, while scoping information (including implicit symbol declarations and scoping error feedback) is stored in the attributes of LogicConcepts, and thus there is really no strict need for an API to manipulate such data, an API makes it easier, more readable, and less error-prone. So we provide the following functions to that end.
- For manipulating implicit symbol declaration data:
- For manipulating feedback about scoping errors:
Second, the main workhorse function of this module is to run a scope checking algorithm on any given Environment. That function is called validate(), and you can read its documentation for greater details.
Note also that this module sort of shares a name with the function
scope() (and its related functions) because for any
Declaration D, the scope of the symbols introduced in
that declaration is precisely D.scope()
, as defined in
the documentation of the scope() function.
Source
Methods
addImplicitDeclaration(target, symbolName)
Given a LogicConcept in which we wish to mark some symbols as implicitly declared, this function writes that data into the appropriate attribute, merging it with any data already there. It is assumed that the implicit symbol declaration data is an array of symbol names, and that the new symbol given here is to be appended to the end of that array, if and only if it wasn't already on the array.
This function is made static so that any client can easily make use of the same convention for storing implicit declarations in a LogicConcept, but the standard way to compute and store such errors is by passing an implicit symbol declaration handler to validate().
Parameters
-
target
LogicConcept
the LogicConcept in which to add a new implicitly declared symbol
-
symbolName
Object
the name of the symbol to mark implicitly declared
Source
addScopeError(target, newData)
Given a LogicConcept in which we wish to save some
data about its scoping errors, this function writes that data into the
appropriate attribute, merging it with any data already there. It is
assumed that the scoping error data is an object containing key-value
pairs as is common in JSON data, and that the newData
will be merged
with any old data already in the target
using Object.assign()
.
This function is made static so that any client can easily make use of the same convention for storing scoping errors in a LogicConcept, but the standard way to compute and store such errors is with validate().
Parameters
-
target
LogicConcept
the LogicConcept in which to add data about scope errors
-
newData
Object
an object that can be treated as JSON data, to be merged with existing scope error data in the
target
Source
clearImplicitDeclarations(target)
This function clears any data stored by addScopeError(). It could just as easily be done with clearAttributes() and the appropriate key, but this API makes it easy to remain consistent if the underlying data storage method were to change.
After this function has been run on the target
, the result of
scopeErrors() for that same target is
guaranteed to be undefined.
Parameters
-
target
LogicConcept
the LogicConcept from which to remove the scope error data
Source
clearScopeErrors(target)
This function clears any data stored by addScopeError(). It could just as easily be done with clearAttributes() and the appropriate key, but this API makes it easy to remain consistent if the underlying data storage method were to change.
After this function has been run on the target
, the result of
scopeErrors() for that same target is
guaranteed to be undefined.
Parameters
-
target
LogicConcept
the LogicConcept from which to remove the scope error data
Source
declareGlobal(symbolName, target)
A callback function that can be used with the
validate() function, as in
Scoping.validate(L,Scoping.declareGlobal)
. If used in that manner, for
every free and undeclared symbol in L
, this function will add an implicit
declaration of it at the top-level ancestor (the "global" scope) for each
free and undeclared symbol. Each symbol is added only once to that global
scope, because implicit symbol declaration lists are actually sets, not
lists.
Parameters
-
symbolName
string
the name of the symbol to implicitly declare
-
target
LogicConcept
the location at which the symbol appears free and undeclared
Source
declareInAncestor(symbolName, target)
A callback function that can be used with the
validate() function, as in
Scoping.validate(L,Scoping.declareInAncestor)
. If used in that manner,
for every free and undeclared symbol in L
, this function will add an
implicit declaration of it at the smallest enclosing
Environment in which the symbol was used free and
undeclared. This behavior is complex enough that it bears illustrating
with a few examples.
Example 1: If the symbol appears free and undeclared in only one expression, then that expression's parent environment is the one in which it will be marked implicitly declared.
Example 2: If the symbol appears free and undeclared in an expression, and then also in another expression that sits in an enclosing environment (an ancestor a few levels above the target), then it will be marked implicitly declared in that outer, ancestor environment, and not anywhere else. Because the implicit declaration acts as a declaration at the start of the enclosing ancestor, its scope includes all the descendants, including both expressions in which it appears free and undeclared.
Example 3: If the symbol appears free and undeclared twice, once in an expression in an environment $E_1$ and the other time in an expression in an environment $E_2$, and both $E_1$ and $E_2$ are children of the same parent, then implicit declarations will be added to both $E_1$ and $E_2$, because they are each the smallest enclosing environment for the free and undeclared instance of the symbol within them.
Parameters
-
symbolName
string
the name of the symbol to implicitly declare
-
target
LogicConcept
the location at which the symbol appears free and undeclared
Source
declareWhenSeen(symbolName, target)
A callback function that can be used with the
validate() function, as in
Scoping.validate(L,Scoping.declareWhenSeen)
. If used in that manner,
for every free and undeclared symbol in L
, this function will add an
implicit declaration of it at the first occurrence of that symbol in any
given scope. (The validate() routine calls
the callback only on the first occurrence in any given scope, so this
function just adds an implicit declaration every time it is called.)
Parameters
-
symbolName
string
the name of the symbol to implicitly declare
-
target
LogicConcept
the location at which the symbol appears free and undeclared
Source
doNotDeclare()
A callback function that can be used with the
validate() function, as in
Scoping.validate(L,Scoping.doNotDeclare)
. Because this function does
absolutely nothing, that code is equivalent to calling
Scoping.validate(L)
, but we provide this function for those times when
the caller wishes to make it obvious, in the code, that implicit
declarations are being expressly omitted.
Source
implicitDeclarations(target) → {Array|undefined}
This function reads the data stored by addImplicitDeclaration(). It could just as easily be done with getAttribute() and the appropriate key, but this API makes it easy to remain consistent if the underlying data storage method were to change.
The result will be an array of symbol names, possibly empty if no symbols
are implicitly declared in the target
.
Parameters
-
target
LogicConcept
the LogicConcept from which to read the implicit symbol declaration data
Returns
-
Array
undefined
the implicit symbol declaration data in the
target
, or undefined if none has been stored there
Source
removeImplicitDeclaration(target, symbolName)
Given a LogicConcept in which we have marked some symbols as implicitly declared, this function modifies data in the target, removing the given symbol from the implicitly declared list in the target. If it isn't already there, this function takes no action.
This function is made static so that any client can easily make use of the same convention for storing implicit declarations in a LogicConcept, but the standard way to compute and store such errors is by passing an implicit symbol declaration handler to validate().
Parameters
-
target
LogicConcept
the LogicConcept from which to remove the implicitly declared symbol
-
symbolName
Object
the name of the symbol to remove
Source
scopeErrors(target) → {Object|undefined}
This function reads the data stored by addScopeError(). It could just as easily be done with getAttribute() and the appropriate key, but this API makes it easy to remain consistent if the underlying data storage method were to change.
The result will be an object, JSON data of key-value pairs, the keys of which will be the types of scoping errors, such as "invalid" for invalid symbol declarations or "undeclared" for variables that appear free and undeclared. If any such key is missing, there are no errors of that type.
Parameters
-
target
LogicConcept
the LogicConcept from which to read the scope error data
Returns
-
Object
undefined
the scope error data in the
target
, or undefined if none has been stored there
Source
validate(target, callbackopt)
Refer to the documentation at the top of this page for information on what scoping is in general. This function is the main workhorse that validates symbol scoping inside an arbitrary Environment. It will typically be called on the topmost environment provided as input to Lurch, so that all symbols in the entire input have been analyzed for where their scopes begin and end.
This function can be called in two ways. First, it can be called with just one argument, in which case only the following tasks are done:
- Any symbol declaration (by a BindingEnvironment
or a Declaration) that declares a symbol $x$ while
sitting in the scope of some other declaration of the same symbol $x$ is
marked with a scoping error of the
form
{ redeclared : [ 'x' ] }
. If more than one symbol is redeclared, the array may contain multiple entries. - Any symbol $x$ bound by a BindingExpression in
the scope of a declaration of the same symbol $x$ or another binding of
the same symbol $x$ is marked with a scoping error of the form
{ redeclared : [ 'x' ] }
. If more than one symbol is redeclared by the binding, the array may contain multiple entries. - Any symbol $x$ used free in an
Expression but not in the scope of any declaration (by
a BindingEnvironment or a
Declaration) is
marked with a scoping error of the form
{ undeclared : [ 'x' ] }
. If more than one symbol is used free and undeclared, the array may contain multiple entries. Such an error is placed only on the first use of the symbol in any given scope; later uses of the same symbol in the same scope are not also marked.
Note that a single LogicConcept might have more than one
type of error. For instance, a Declaration that attempts
to re-declare a symbol $x$, and whose body contains the symbols $y$ and $z$
free and undeclared might have an error of the form
{ redeclared : [ 'x' ], undeclared : [ 'y', 'z' ] }
.
There are a variety of sensible ways to handle variables that are free and
undeclared. On the permissive end of the spectrum, a client may wish to
simply ignore them, in which case the error feedback under the undeclared
headings can be ignored. On the strict end of the spectrum, a client may
refuse to process input that contains free and undeclared symbols. Such
clients can pay particular attention to the feedback under the undeclared
headings. But many clients will take a middle road of some type, such as
treating the first use of the symbol as an implicit declaration of the
symbol, either at that location, or at the beginning of its parent
environment, or something else.
To help clients who wish to take this middle road, this function has an optional second argument. If a function is provided as the second argument, it will be called on the first instance of every free and undeclared symbol before the validation work described above is executed. The function will be passed two parameters at each call: the name of the symbol that appears free and undeclared, and the LogicConcept in which it appears. These calls will take place in tree-traversal order (that is, earlier nodes first).
The callback function will often want to mark some LogicConcept as the location of the implicit declaration of the symbol in question. It can do so by calling addImplicitDeclaration(). Note that an implicit declaration marked in a LogicConcept L takes effect exactly at L, in the sense that the scope of the declaration includes L itself and continues on through the rest of L's scope.
While callers can provide a callback function for whatever behavior they desire, we also provide some pre-built callback functions, for common actions the caller may desire: declareWhenSeen(), declareInAncestor(), declareGlobal(), and doNotDeclare().
Parameters
-
target
LogicConcept
the LogicConcept in which to do the work
-
callback
function
<optional>
the handler for all symbols that are eligible for implicit declaration, as described above