/**
* Parse a string to convert it to an LC and process Shorthands that appear in
* an LC.
*
* @module Parsing
*/
//////////////////////////////////////////////////////////////////////////////
//
// Parsers and Parsing Utilties
// for converting strings to LCs
// and vice versa
//
//////////////////////////////////////////////////////////////////////////////
//
// Imports
//
import { Environment } from '../core/src/environment.js'
import { Symbol as LurchSymbol } from '../core/src/symbol.js'
import { Application } from '../core/src/application.js'
import Algebrite from '../dependencies/algebrite.js'
const compute = Algebrite.run
import './extensions.js'
/**
* Make both a normal and tracing peggy parser from the given string and capture
* and customize the error formatting, then return both parsers and the original
* parser (which throws errors but doesn't trace) in an array.
* @function
* @param {string} parserstr - the peggy parser definition string
* @returns {function[]} - the normal, tracing, and raw parsers
*/
export const makeParser = parserstr => {
const opts = { cache:true }
const traceopts = { ...opts , trace:true }
const rawparser = peggy.generate(parserstr,opts)
const rawtraceparser = peggy.generate(parserstr,traceopts)
const parser = (s,opts) => {
try {
return rawparser.parse(s,opts)
} catch(e) {
if (typeof e.format === 'function') {
console.log(e.format([{
grammarSource:parserstr,
text:s
}]))
} else {
console.log(e.toString())
}
return undefined
}
}
// No need for this in a browser
const traceparser = (typeof global !== 'undefined' ) ?
s => {
// make the backtracer
const tracer = new Tracer(s,
{ showTrace : true,
showFullPath : false,
hiddenPaths : [ "__" , "_" ]
}
)
// show backtracing whether it's an error or not
try {
const ans = rawtraceparser.parse(s,{ tracer:tracer })
write(tracer.getBacktraceString())
return ans
} catch(e) {
write(tracer.getBacktraceString())
return undefined
}
} : undefined
return { parse: parser, trace: traceparser, raw: rawparser.parse }
}
// Take a text file that has n lines and parse it one line at a time, optionally
// showing how each line parses, and reporting on the number of lines that
// failed to parse. The main use is to take a file with LurchNotation lines and
// convert it to either putdown or latex, and print the results as a test of the
// parser.
export const parseLines = (parser,verbose=true,name='LurchParserTests',opts) => {
let ans = []
const lines =
loadStr(name,'../parsers/','lurch').split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && line.slice(0,2)!=='//')
// console.log(`File contains ${lines.length} parseable lines`)
let pass = 0, fail = 0
let report = []
lines.forEach( l => {
try {
ans.push(parser(l,opts))
pass++
if (verbose) write(`${Pens.itemPen(l)}\n → ${Pens.stringPen(parser(l,opts))}\n`)
} catch {
report.push(l)
fail++
}
})
report.forEach(l=>console.log(`Could not parse ${Pens.contextPen(l)}`))
console.log(`Parsed ${pass} lines successfully, ${fail} failed`)
return (verbose) ? ans : undefined
}
// This was a prototype made quickly at the AIM workshop We keep it for now but
// improve on it below.
export const lc2algebrite = e => {
if (e instanceof Application &&
e.numChildren()==3 &&
e.child(0) instanceof LurchSymbol &&
e.child(0).text()==='is' &&
e.child(2) instanceof LurchSymbol &&
e.child(2).text()==='prime' &&
e.child(1) instanceof LurchSymbol &&
e.child(1).text().length > 0 &&
Number.isInteger(Number(e.child(1).text())) ) {
const n = Number(e.child(1).text())
return `isprime(${n})`
} else {
return '0'
}
}
///////////////////////////////////////////////////////////////////
// Arithmetic
//
// For now, the only three relations we allow
const NumericRelns = ['=','<','≤']
// utilities
// check if this LC is non-negative
const isNonnegative = e => {
return isNumeric(e) && (compute(`0<=${numericToCAS(e)}`)==='1')
}
// check if this LC is non-zero
const isNonzero = e => {
return isNumeric(e) && (compute(`0==${numericToCAS(e)}`)==='0')
}
// check if this LC isRational, but mathematically reduces to an integer
const isReducedInteger = e => {
return isNumeric(e) && (compute(`denominator(${numericToCAS(e)})`)==='1')
}
// Natural Numbers
//
// Check if this LC represents a natural constant
const isNaturalNumber = n => n.matches('[1-9][0-9]*$|^0')
// Check if this expression is a natural number expression
const isNatural = e => {
// base case - natural numbers are always natural expressions
if (isNaturalNumber(e)) return true
// recursion - if it is not an application, we're done
if ( ! (e instanceof Application) ) return false
// it is an Application so get the op
const op = e.child(0)
const nargs = e.numChildren()
// check the binary ops
if (['\\+','⋅','\\^'].some(x=>op.matches(x)) && nargs == 3)
return e.children().slice(1).every(isNatural)
// check unary ops
if (op.matches('!') && nargs == 2)
return isNatural(e.child(1))
// otherwise it's not a supported operator
return false
}
// Check if this expression can be evaluated by natural number arithmetic
const isNaturalArithmetic = e => {
// it must be an application with three children...
return e instanceof Application && e.numChildren() === 3 &&
// and it's operator must be one of the three...
NumericRelns.some( reln => e.child(0).matches(reln)) &&
// .. and the rest of the arguments are natural expressions
e.children().slice(1).every(c=>isNatural(c))
}
// Integers
//
// Since negation is an operator, the constants are the same as for
// Natural Numbers
// Check if this expression is an integer expression
const isInteger = e => {
// base case - natural numbers are always integer expressions
if (isNaturalNumber(e)) return true
// recursion - if it is not an application, we're done
if ( ! (e instanceof Application) ) return false
// it is an Application so get the op
const op = e.child(0)
// and the arity
const nargs = e.numChildren()
// check the easy binary ops
if (['\\+','⋅'].some(x=>op.matches(x)) && nargs == 3 &&
e.children().slice(1).every(isInteger)) return true
// for ^ check that the second arg is non-negative
if (op.matches('\\^') && nargs == 3 &&
e.children().slice(1).every(isInteger)) {
return isNonnegative(e.child(2))
}
// check unary ops
// negation can have any argument
if (op.matches('-') && nargs == 2 ) return isInteger(e.child(1))
// factorial has to have nonnegative argument
if (op.matches('!') && nargs == 2 )
return isInteger(e.child(1)) && isNonnegative(e.child(1))
// otherwise it's not a supported operator
return false
}
// Check if this expression can be evaluated by natural number arithmetic
const isIntegerArithmetic = e => {
// it must be an application with three children...
return e instanceof Application && e.numChildren() === 3 &&
// and it's operator must be one of the three...
NumericRelns.some( reln => e.child(0).matches(reln)) &&
// .. and the rest of the arguments are natural expressions
e.children().slice(1).every(c=>isInteger(c))
}
// Rational
//
// Since negation and division are operators, the constants are the same as for
// Natural Numbers
// Check if this expression is an integer expression
const isRational = e => {
// base case - natural numbers are always integer expressions
if (isNaturalNumber(e)) return true
// recursion - if it is not an application, we're done
if ( ! (e instanceof Application) ) return false
// it is an Application so get the op
const op = e.child(0)
// and the arity
const nargs = e.numChildren()
// check the easy binary ops
if (['\\+','⋅'].some(x=>op.matches(x)) && nargs == 3 &&
e.children().slice(1).every(isRational)) return true
// for ^ check that the second arg is an integer expression (no roots)
// Note: we don't allow rational expressions that simplify to an integer
if (op.matches('\\^') && nargs == 3 &&
e.children().slice(1).every(isRational)) {
return isReducedInteger(e.child(2)) // this should allow rational that is integer... TODO
}
// check unary ops
// for / check that the second arg is nonzero
if (op.matches('/') && nargs == 2 &&
isRational(e.child(1))) {
return isNonzero(e.child(1))
}
// negation can have any argument
if (op.matches('-') && nargs == 2 ) return isRational(e.child(1))
// factorial has to have nonnegative integer argument
if (op.matches('!') && nargs == 2 )
return isInteger(e.child(1)) && isNonnegative(e.child(1))
// otherwise it's not a supported operator
return false
}
// Check if this expression can be evaluated by natural number arithmetic
const isRationalArithmetic = e => {
// it must be an application with three children...
return e instanceof Application && e.numChildren() === 3 &&
// and it's operator must be one of the three...
NumericRelns.some( reln => e.child(0).matches(reln)) &&
// .. and the rest of the arguments are natural expressions
e.children().slice(1).every(c=>isRational(c))
}
// Since naturals and integers are special cases of rationals, they are all numerics
const isNumeric = isRational
// Check if this LC represents an equality (=) or inequality (< , ≤) of numeric
// expressions in the given ring. This allows us to specify the ring independently.
export const isArithmetic = {
ℕ: isNaturalArithmetic,
ℤ: isIntegerArithmetic,
ℚ: isRationalArithmetic
}
// convert an Arithmetic statement in a ring to a CAS input string
// this assumes you have already checked that the Arithmetic is in the appropriate ring
export const arithmeticToCAS = e => {
const ans = numericToCAS(e.child(1))+e.child(0).text()+numericToCAS(e.child(2))
return ans.replace(/=/g,'==')
.replace(/≤/g,'<=')
}
// convert an LC that isNumeric into a CAS input string
const numericToCAS = e => {
// just return if it isn't a Numeric Expression
if (!isNumeric(e)) return
// syntactic sugar
const convert = numericToCAS
// if it's a number, return its text
if (isNaturalNumber(e)) return e.text()
// It isn't a number so it must be compound
const kids = e.children()
// binary infix ops (all for now)
if (kids.length===3) {
return `(${convert(kids[1])}${kids[0].text()}${convert(kids[2])})`.replace(/⋅/g,'*')
// unary on the right
} else if (kids[0].matches('!')) {
return `(${convert(kids[1])}!)`
// unary on the left
// division is unary reciprocal
} else if (kids[0].matches('/')) {
return `(1/${convert(kids[1])})`
// negation is what's left
} else {
return `(${kids[0].text()}${convert(kids[1])})`
}
}
/**
* ## Process Shorthands
*
* In order to make it convenient to enter large documents in putdown notation,
* it is convenient to use fromPutdown to enter some reserved content in the
* document that is preprocessed before evaluating the document.
*
* The following are what we have for Shorthands. More might be added later.
*
* * Scan a document looking for any of the following Shorthands and convert
* the next (>) or previous (<) sibling to the corresponding type in the asA
* column.
*
* | Shorthand | mark asA |
* | ------------|-----------|
* | 'BIH>' | 'BIH' |
* | 'declare>' | 'Declare' |
* | 'rule>' | 'Rule' |
* | 'cases>' | 'Cases' |
* | 'subs>' | 'Subs' |
* | 'thm>' | 'Theorem' |
* | '<thm' | 'Theorem' |
* | 'proof>' | 'Proof' |
* | 'proof>' | 'Proof' |
*
* * Scan for occurrences of the symbol `rules>`. Its next sibling should be
* an environment containing given Environments. Mark each child of the next
* sibling as a `Rule` and delete both the `rules>` symbol and the outer
* environment containing the newly marked `Rules. This allows us to use an
* Environment to mark a lot of consecutive `Rules` all at once and then
* ignore the wrapper Environment. For libraries this is cleaner than trying
* to mark every Rule with `rule>` individually.
*
* * Scan for occurrences of the symbol `λ` (or `@` for backwards
* compatibility) and replace that with the symbol "LDE EFA" (which then
* will still print as '𝜆' but it's what is needed under the hood).
*
* * Scan for occurrences of the symbol `≡`. They are intended to be a
* shorthand way to enter IFF rules (equivalences). The '≡' should be a
* child of a Rule environment, and should not be the first or last child.
* The Rule will then be replaced by the expanded version and the `≡`
* symbols removed, following the cyclic TFAE style of implications. For
* example, if the Rule has the form `:{ a ≡ b c ≡ d }` then it will be
* replaced by `:{ {:a {b c}} {:{b c} d } {:d a} }`.
*
* * Scan for occurrences of the symbol `➤`. If found it should be the first
* child of an Application whose second child is a symbol whose text is the
* text of the comment. Mark that Application with `.ignore=true` so it is
* ignored propositionally.
*
* * Scan for occurrences of the symbol `by` and mark its previous sibling's
* `.by` attribute with the text of its next sibling, which must be a
* LurchSymbol. Then delete both the `by` and it's next sibling. Currently
* used by the `Cases` tool and the CAS tool.
*
* * Scan for occurrences of the symbol `✔︎`, `✗`, and `⁉︎` and mark its
* previous sibling with .expectedResult 'valid', 'indeterminate', and
* 'invalid' respectively.
*
* * Scan a document looking for the symbol `<<`, which we call a 'marker'.
* For every marker,
* - if the preceding sibling is an environment, attribute it as a `BIH`.
*
* - if the preceding sibling is a declaration, attribute it as a `Declare`,
*
* - in either case, finally, delete the marker.
*
* Naturally we have to run this FIRST before anything else. These changes are
* made in-place - they don't return a copy of the document.
*
* This does no error checking, so << has to be an outermost expression with a
* previous sibling and λ has to appear in some sensible location and so on.
*
* @function
* @param {Environment} L - the document
* @returns {LogicConcept} - the modified document
*/
export const processShorthands = L => {
// for each symbol named symb, do f, i.e. execute f(symb)
const processSymbol = ( symb , f ) => {
L.descendantsSatisfying( x => (x instanceof LurchSymbol) && x.text()===symb )
.forEach( s => f(s) )
}
// make next sibling have a given type. If the optional third argument is missing, do nothing further. If flag is 'given' make the target a given. If the flag is 'claim' make the target a claim.
const makeNext = (m,type,flag) => {
const next = m.nextSibling()
next.makeIntoA(type)
if (flag === 'given') next.makeIntoA('given')
if (flag === 'claim') next.unmakeIntoA('given')
m.remove()
}
// make previous sibling have a given type
const makePrevious = (m,type) => {
m.previousSibling().makeIntoA(type)
m.remove()
}
// In addition to processing Symbols, we also want to sometimes react
// to the presence of certain attributes.
// Move ExpectedResult LC attributes to js attributes. The reason to do this
// is so we can use .lurch files created with the web UI as test files by
// exporting them after validating (using the CMD+SHIFT+D option). That has
// to attribute the LC's with an LC attribute.
//
// To use such a file as a test file we must validate it and compare the
// ExpectedResult's with the actual results. However, the process of
// validation makes copies of some LC's, like the user-rules created for each
// Theorem and the ForSome bodies.
//
// But the LC copy method copies it's LC
// attributes producing items marked with ExpectedResults that do not have any
// result at all from validation. By moving the LC attribute to a js attribute
// that fixes that problem because the LC copy routine does not copy js
// attributes on the LC.
L.descendantsSatisfying( x => x.hasAttribute('ExpectedResult'))
.forEach( s => {
s.ExpectedResult = s.getAttribute('ExpectedResult')
s.clearAttributes('ExpectedResult')
} )
// declare the type of the next or previous sibling
processSymbol( 'BIH>' , m => makeNext(m,'BIH','claim') )
processSymbol( 'declare>' , m => makeNext(m,'Declare','given') )
processSymbol( 'rule>' , m => makeNext(m,'Rule','given') )
processSymbol( 'thm>' , m => makeNext(m,'Theorem','claim') )
processSymbol( '<thm' , m => makePrevious(m,'Theorem','claim') )
processSymbol( 'proof>' , m => makeNext(m,'Proof','claim') )
processSymbol( 'cases>' , m => makeNext(m,'Cases','given') )
// Mark a rule as the substitution rule, and mark it's conclusion as a
// substitution EFA so that it can be instantiated by expressions marked
// with .by='substitution'
processSymbol( 'subs>' , m => {
m.nextSibling().conclusions().forEach( c => c.makeIntoA('Subs'))
makeNext(m,'Subs','given')
})
// attribute the previous sibling with .by attribute whose value is the text
// of the next sibling if it is a symbol, and the next sibling LC itself if it isn't.
processSymbol( 'by' , m => {
let LHS = m.previousSibling()
// for testing purposes if the previous sibling is an 'expected result' marker
// attribute its previous sibling instead
if (['✔︎','✗','⁉︎','⊘'].some(x=>LHS.matches(x))) LHS = LHS.previousSibling()
const RHS = m.nextSibling()
// it should be a LurchSymbol or an Application
if (RHS instanceof LurchSymbol) {
if (RHS.text()==='CAS') {
LHS.by = { CAS: lc2algebrite(LHS) }
m.remove()
RHS.remove()
} else {
LHS.by = RHS.text()
m.remove()
RHS.remove()
}
} else if (RHS instanceof Application ) {
if (RHS.child(0) instanceof LurchSymbol && RHS.child(0).text()==='CAS') {
// TODO: handle the case where no arg is passed or it's not a symbol
LHS.by = { CAS: RHS.child(1).text() }
m.remove()
RHS.remove()
}
}
return
} )
// rules> - Mark each of the children of the next sibling (which should be an
// environment) as a Rule, and delete both the shorthand and the environment.
processSymbol( 'rules>' , m => {
const wrapper = m.nextSibling()
wrapper.children().forEach( kid => {
if (kid instanceof Environment) {
kid.makeIntoA('Rule')
kid.makeIntoA('given')
// the rule has no creators
kid.creators=[]
// TODO: the following would be useful for web UI but not for
// Lode, since I've used claim environments in rules.
// kid.children().forEach( premise => {
// if (premise instanceof Environment) {
// premise.makeIntoA('given')
// }
// })
}
wrapper.shiftChild()
kid.insertBefore(wrapper)
} )
wrapper.remove()
m.remove()
} )
// simple replacements
processSymbol( 'λ' , m => {
m.replaceWith(new LurchSymbol('LDE EFA'))
} )
processSymbol( '@' , m => {
m.replaceWith(new LurchSymbol('LDE EFA'))
} )
// Expand equivalences
processSymbol( '≡' , m => {
// find the parent environment, if there is none, then do nothing
const parent = m.parent()
if (!parent) return
// a utility to identify equivalence separators
const isSeparator = x => x instanceof LurchSymbol && x.text() === '≡'
// get the children of the parent
let inputArray = parent.children()
// an array to hold the groups
let groups = []
// while there are separators, split the input array into groups
let k = inputArray.findIndex( isSeparator )
while ( k !== -1) {
if (k==1) groups.push(inputArray[0])
else groups.push(inputArray.slice(0,k))
inputArray = inputArray.slice(k+1)
k = inputArray.findIndex( isSeparator )
}
// if there are no more separators, then push what's left
if (inputArray.length === 1) groups.push(inputArray[0])
else groups.push(inputArray)
// for each group, if it is an array, create a new Environment containing the group elements, otherwise just use the element itself. Collect them all into a results array.
const results = []
groups.forEach( group => {
if (Array.isArray(group) ) {
const newEnv = new Environment( ...group )
results.push(newEnv)
} else {
results.push(group)
}
})
// finally, replace the parent with a new environment containing all of the
// cyclic implications.
const ans = new Environment()
ans.copyAttributesFrom(parent)
// put all of the pairs into the new environment except the last one
results.slice(0,-1).forEach( ( result, i ) => {
let myEnv = new Environment( result.asA('given') ,
results[i+1].copy().unmakeIntoA('given') )
ans.pushChild(myEnv)
} )
// and complete the cycle with the last one
ans.pushChild(new Environment(
results[results.length-1].asA('given'),
results[0].copy().unmakeIntoA('given') ) )
// replace the parent with the new environment
parent.replaceWith(ans)
} )
// For testing purposes, flag the expected result
processSymbol( '✔︎' , m => {
m.previousSibling().ExpectedResult = 'valid'
m.remove()
} )
processSymbol( '✗' , m => {
m.previousSibling().ExpectedResult = 'indeterminate'
m.remove()
} )
processSymbol( '⁉︎' , m => {
m.previousSibling().ExpectedResult = 'invalid'
m.remove()
} )
processSymbol( '⊘' , m => {
m.previousSibling().ExpectedResult = 'inapplicable'
m.remove()
} )
// TODO: make this more consistent with the other shorthands
processSymbol( '➤' , m => {
if (m.parent().isAComment()) m.parent().ignore=true
})
// Labels
//
// Just a quickie lable mechanism that will be upgraded later.
// Labels are currently a single symbol of the form name> which assigns
// 'name' to the .label attribute of the next sibling. Previous siblings
// aren't supported yet, nor is whitespace.
// L.descendantsSatisfying( s => (s instanceof LurchSymbol) &&
// /[^"()\[\]\s]+>/.test(s.text()) )
// .forEach( s => {
// s.nextSibling().label=s.text().slice(0,-2).toLowerCase()
// s.remove()
// } )
// depricated but kept for backward compatibility
processSymbol( '<<' , m => {
const target = m.previousSibling()
const type = (target instanceof Declaration) ? 'Declare' : 'BIH'
target.makeIntoA(type)
m.remove()
} )
// depricated but kept for backward compatibility
processSymbol( '>>' , m => makeNext(m,'BIH') )
return L
}
export default {
isNonnegative, isNonzero, isNaturalNumber, isNatural, isInteger, isRational,
isNumeric, isNaturalArithmetic, isIntegerArithmetic, isRationalArithmetic,
numericToCAS
}
///////////////////////////////////////////////////////////////////////////////
source