summaryrefslogtreecommitdiff
path: root/macros/luatex/latex/luacas/tex
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/latex/luacas/tex')
-rw-r--r--macros/luatex/latex/luacas/tex/_lib/inspect.lua335
-rw-r--r--macros/luatex/latex/luacas/tex/_lib/pepperfish.lua629
-rw-r--r--macros/luatex/latex/luacas/tex/_lib/table.lua185
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/_init.lua24
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/absexpression.lua80
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/equation.lua183
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/euclideandomain.lua63
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/factorialexpression.lua102
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/field.lua69
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/integer.lua952
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/integerquotientring.lua197
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/logarithm.lua186
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/polynomialring.lua860
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/polynomialring/berlekampfactoring.lua187
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/polynomialring/decomposition.lua93
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/polynomialring/zassenhausfactoring.lua220
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/rational.lua241
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/ring.lua326
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/rootexpression.lua135
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/sqrtexpression.lua196
-rw-r--r--macros/luatex/latex/luacas/tex/algebra/trigexpression.lua355
-rw-r--r--macros/luatex/latex/luacas/tex/calculus/_init.lua6
-rw-r--r--macros/luatex/latex/luacas/tex/calculus/derivativeexpression.lua265
-rw-r--r--macros/luatex/latex/luacas/tex/calculus/diffexpression.lua190
-rw-r--r--macros/luatex/latex/luacas/tex/calculus/integralexpression.lua934
-rw-r--r--macros/luatex/latex/luacas/tex/core/_init.lua16
-rw-r--r--macros/luatex/latex/luacas/tex/core/atomicexpression.lua70
-rw-r--r--macros/luatex/latex/luacas/tex/core/binaryoperation.lua800
-rw-r--r--macros/luatex/latex/luacas/tex/core/binaryoperation/difference.lua14
-rw-r--r--macros/luatex/latex/luacas/tex/core/binaryoperation/power.lua169
-rw-r--r--macros/luatex/latex/luacas/tex/core/binaryoperation/product.lua231
-rw-r--r--macros/luatex/latex/luacas/tex/core/binaryoperation/quotient.lua14
-rw-r--r--macros/luatex/latex/luacas/tex/core/binaryoperation/sum.lua226
-rw-r--r--macros/luatex/latex/luacas/tex/core/compoundexpression.lua52
-rw-r--r--macros/luatex/latex/luacas/tex/core/constantexpression.lua62
-rw-r--r--macros/luatex/latex/luacas/tex/core/expression.lua280
-rw-r--r--macros/luatex/latex/luacas/tex/core/functionexpression.lua295
-rw-r--r--macros/luatex/latex/luacas/tex/core/symbolexpression.lua132
-rw-r--r--macros/luatex/latex/luacas/tex/luacas.sty312
-rw-r--r--macros/luatex/latex/luacas/tex/test/calculus/derivatives.lua30
-rw-r--r--macros/luatex/latex/luacas/tex/test/calculus/integrals.lua54
-rw-r--r--macros/luatex/latex/luacas/tex/test/expressions/autosimplify.lua199
-rw-r--r--macros/luatex/latex/luacas/tex/test/expressions/collect.lua26
-rw-r--r--macros/luatex/latex/luacas/tex/test/expressions/equations.lua17
-rw-r--r--macros/luatex/latex/luacas/tex/test/expressions/functions.lua17
-rw-r--r--macros/luatex/latex/luacas/tex/test/expressions/logarithms.lua26
-rw-r--r--macros/luatex/latex/luacas/tex/test/expressions/rationalexponent.lua15
-rw-r--r--macros/luatex/latex/luacas/tex/test/expressions/simplify.lua22
-rw-r--r--macros/luatex/latex/luacas/tex/test/expressions/substitute.lua10
-rw-r--r--macros/luatex/latex/luacas/tex/test/helper.lua322
-rw-r--r--macros/luatex/latex/luacas/tex/test/main.lua154
-rw-r--r--macros/luatex/latex/luacas/tex/test/parser.lua323
-rw-r--r--macros/luatex/latex/luacas/tex/test/polynomials/partialfractions.lua12
-rw-r--r--macros/luatex/latex/luacas/tex/test/polynomials/polynomial.lua153
-rw-r--r--macros/luatex/latex/luacas/tex/test/polynomials/polynomialmod.lua76
-rw-r--r--macros/luatex/latex/luacas/tex/test/polynomials/roots.lua43
-rw-r--r--macros/luatex/latex/luacas/tex/test/rings/conversion.lua273
-rw-r--r--macros/luatex/latex/luacas/tex/test/rings/modulararithmetic.lua20
-rw-r--r--macros/luatex/latex/luacas/tex/test/rings/number.lua118
59 files changed, 11596 insertions, 0 deletions
diff --git a/macros/luatex/latex/luacas/tex/_lib/inspect.lua b/macros/luatex/latex/luacas/tex/_lib/inspect.lua
new file mode 100644
index 0000000000..dedd91f248
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/_lib/inspect.lua
@@ -0,0 +1,335 @@
+local inspect ={
+ _VERSION = 'inspect.lua 3.1.0',
+ _URL = 'http://github.com/kikito/inspect.lua',
+ _DESCRIPTION = 'human-readable representations of tables',
+ _LICENSE = [[
+ MIT LICENSE
+
+ Copyright (c) 2013 Enrique GarcĂ­a Cota
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ ]]
+}
+
+local tostring = tostring
+
+inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end})
+inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end})
+
+local function rawpairs(t)
+ return next, t, nil
+end
+
+-- Apostrophizes the string if it has quotes, but not aphostrophes
+-- Otherwise, it returns a regular quoted string
+local function smartQuote(str)
+ if str:match('"') and not str:match("'") then
+ return "'" .. str .. "'"
+ end
+ return '"' .. str:gsub('"', '\\"') .. '"'
+end
+
+-- \a => '\\a', \0 => nil
+local shortControlCharEscapes = {
+ ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
+ ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127",
+}
+local longControlCharEscapes = {["\127"]="\127"} -- \a => nil, \0 => \000, 31 => \031
+for i=0, 31 do
+ local ch = string.char(i)
+ if not shortControlCharEscapes[ch] then
+ shortControlCharEscapes[ch] = "\\"..i
+ longControlCharEscapes[ch] = string.format("\\%03d", i)
+ end
+end
+--longControlCharEscapes["\127"]="\\127"
+
+local function escape(str)
+ return (str:gsub("\\", "\\\\")
+ :gsub("(%c)%f[0-9]", longControlCharEscapes)
+ :gsub("%c", shortControlCharEscapes))
+end
+
+local function isIdentifier(str)
+ return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
+end
+
+local function isSequenceKey(k, sequenceLength)
+ return type(k) == 'number'
+ and 1 <= k
+ and k <= sequenceLength
+ and math.floor(k) == k
+end
+
+local defaultTypeOrders = {
+ ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
+ ['function'] = 5, ['userdata'] = 6, ['thread'] = 7
+}
+
+local function sortKeys(a, b)
+ local ta, tb = type(a), type(b)
+
+ -- strings and numbers are sorted numerically/alphabetically
+ if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
+
+ local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
+ -- Two default types are compared according to the defaultTypeOrders table
+ if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
+ elseif dta then return true -- default types before custom ones
+ elseif dtb then return false -- custom types after default ones
+ end
+
+ -- custom types are sorted out alphabetically
+ return ta < tb
+end
+
+-- For implementation reasons, the behavior of rawlen & # is "undefined" when
+-- tables aren't pure sequences. So we implement our own # operator.
+local function getSequenceLength(t)
+ local len = 1
+ local v = rawget(t,len)
+ while v ~= nil do
+ len = len + 1
+ v = rawget(t,len)
+ end
+ return len - 1
+end
+
+local function getNonSequentialKeys(t)
+ local keys, keysLength = {}, 0
+ local sequenceLength = getSequenceLength(t)
+ for k,_ in rawpairs(t) do
+ if not isSequenceKey(k, sequenceLength) then
+ keysLength = keysLength + 1
+ keys[keysLength] = k
+ end
+ end
+ table.sort(keys, sortKeys)
+ return keys, keysLength, sequenceLength
+end
+
+local function countTableAppearances(t, tableAppearances)
+ tableAppearances = tableAppearances or {}
+
+ if type(t) == 'table' then
+ if not tableAppearances[t] then
+ tableAppearances[t] = 1
+ for k,v in rawpairs(t) do
+ countTableAppearances(k, tableAppearances)
+ countTableAppearances(v, tableAppearances)
+ end
+ countTableAppearances(getmetatable(t), tableAppearances)
+ else
+ tableAppearances[t] = tableAppearances[t] + 1
+ end
+ end
+
+ return tableAppearances
+end
+
+local copySequence = function(s)
+ local copy, len = {}, #s
+ for i=1, len do copy[i] = s[i] end
+ return copy, len
+end
+
+local function makePath(path, ...)
+ local keys = {...}
+ local newPath, len = copySequence(path)
+ for i=1, #keys do
+ newPath[len + i] = keys[i]
+ end
+ return newPath
+end
+
+local function processRecursive(process, item, path, visited)
+ if item == nil then return nil end
+ if visited[item] then return visited[item] end
+
+ local processed = process(item, path)
+ if type(processed) == 'table' then
+ local processedCopy = {}
+ visited[item] = processedCopy
+ local processedKey
+
+ for k,v in rawpairs(processed) do
+ processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
+ if processedKey ~= nil then
+ processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
+ end
+ end
+
+ local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
+ if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field
+ setmetatable(processedCopy, mt)
+ processed = processedCopy
+ end
+ return processed
+end
+
+
+
+-------------------------------------------------------------------
+
+local Inspector = {}
+local Inspector_mt = {__index = Inspector}
+
+function Inspector:puts(...)
+ local args = {...}
+ local buffer = self.buffer
+ local len = #buffer
+ for i=1, #args do
+ len = len + 1
+ buffer[len] = args[i]
+ end
+end
+
+function Inspector:down(f)
+ self.level = self.level + 1
+ f()
+ self.level = self.level - 1
+end
+
+function Inspector:tabify()
+ self:puts(self.newline, string.rep(self.indent, self.level))
+end
+
+function Inspector:alreadyVisited(v)
+ return self.ids[v] ~= nil
+end
+
+function Inspector:getId(v)
+ local id = self.ids[v]
+ if not id then
+ local tv = type(v)
+ id = (self.maxIds[tv] or 0) + 1
+ self.maxIds[tv] = id
+ self.ids[v] = id
+ end
+ return tostring(id)
+end
+
+function Inspector:putKey(k)
+ if isIdentifier(k) then return self:puts(k) end
+ self:puts("[")
+ self:putValue(k)
+ self:puts("]")
+end
+
+function Inspector:putTable(t)
+ if t == inspect.KEY or t == inspect.METATABLE then
+ self:puts(tostring(t))
+ elseif self:alreadyVisited(t) then
+ self:puts('<table ', self:getId(t), '>')
+ elseif self.level >= self.depth then
+ self:puts('{...}')
+ else
+ if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
+
+ local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t)
+ local mt = getmetatable(t)
+
+ self:puts('{')
+ self:down(function()
+ local count = 0
+ for i=1, sequenceLength do
+ if count > 0 then self:puts(',') end
+ self:puts(' ')
+ self:putValue(t[i])
+ count = count + 1
+ end
+
+ for i=1, nonSequentialKeysLength do
+ local k = nonSequentialKeys[i]
+ if count > 0 then self:puts(',') end
+ self:tabify()
+ self:putKey(k)
+ self:puts(' = ')
+ self:putValue(t[k])
+ count = count + 1
+ end
+
+ if type(mt) == 'table' then
+ if count > 0 then self:puts(',') end
+ self:tabify()
+ self:puts('<metatable> = ')
+ self:putValue(mt)
+ end
+ end)
+
+ if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing }
+ self:tabify()
+ elseif sequenceLength > 0 then -- array tables have one extra space before closing }
+ self:puts(' ')
+ end
+
+ self:puts('}')
+ end
+end
+
+function Inspector:putValue(v)
+ local tv = type(v)
+
+ if tv == 'string' then
+ self:puts(smartQuote(escape(v)))
+ elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
+ tv == 'cdata' or tv == 'ctype' then
+ self:puts(tostring(v))
+ elseif tv == 'table' then
+ self:putTable(v)
+ else
+ self:puts('<', tv, ' ', self:getId(v), '>')
+ end
+end
+
+-------------------------------------------------------------------
+
+function inspect.inspect(root, options)
+ options = options or {}
+
+ local depth = options.depth or math.huge
+ local newline = options.newline or '\n'
+ local indent = options.indent or ' '
+ local process = options.process
+
+ if process then
+ root = processRecursive(process, root, {}, {})
+ end
+
+ local inspector = setmetatable({
+ depth = depth,
+ level = 0,
+ buffer = {},
+ ids = {},
+ maxIds = {},
+ newline = newline,
+ indent = indent,
+ tableAppearances = countTableAppearances(root)
+ }, Inspector_mt)
+
+ inspector:putValue(root)
+
+ return table.concat(inspector.buffer)
+end
+
+setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })
+
+return inspect
+
diff --git a/macros/luatex/latex/luacas/tex/_lib/pepperfish.lua b/macros/luatex/latex/luacas/tex/_lib/pepperfish.lua
new file mode 100644
index 0000000000..4717543c4b
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/_lib/pepperfish.lua
@@ -0,0 +1,629 @@
+---@diagnostic disable: param-type-mismatch, lowercase-global, need-check-nil, assign-type-mismatch
+--[[
+
+== Introduction ==
+
+ Note that this requires os.clock(), debug.sethook(),
+ and debug.getinfo() or your equivalent replacements to
+ be available if this is an embedded application.
+
+ Example usage:
+
+ profiler = newProfiler()
+ profiler:start()
+
+ < call some functions that take time >
+
+ profiler:stop()
+
+ local outfile = io.open( "profile.txt", "w+" )
+ profiler:report( outfile )
+ outfile:close()
+
+== Optionally choosing profiling method ==
+
+The rest of this comment can be ignored if you merely want a good profiler.
+
+ newProfiler(method, sampledelay):
+
+If method is omitted or "time", will profile based on real performance.
+optionally, frequency can be provided to control the number of opcodes
+per profiling tick. By default this is 100000, which (on my system) provides
+one tick approximately every 2ms and reduces system performance by about 10%.
+This can be reduced to increase accuracy at the cost of performance, or
+increased for the opposite effect.
+
+If method is "call", will profile based on function calls. Frequency is
+ignored.
+
+
+"time" may bias profiling somewhat towards large areas with "simple opcodes",
+as the profiling function (which introduces a certain amount of unavoidable
+overhead) will be called more often. This can be minimized by using a larger
+sample delay - the default should leave any error largely overshadowed by
+statistical noise. With a delay of 1000 I was able to achieve inaccuray of
+approximately 25%. Increasing the delay to 100000 left inaccuracy below my
+testing error.
+
+"call" may bias profiling heavily towards areas with many function calls.
+Testing found a degenerate case giving a figure inaccurate by approximately
+20,000%. (Yes, a multiple of 200.) This is, however, more directly comparable
+to common profilers (such as gprof) and also gives accurate function call
+counts, which cannot be retrieved from "time".
+
+I strongly recommend "time" mode, and it is now the default.
+
+== History ==
+
+2021-01-04 - Larry Deaton ( larry.deaton@dynetics.com )
+ Modified the profiling by "call" operation to ignore internal LUA functions
+ since LUA only provides debug hooks for the calling of the function and not
+ the return of the function. Without this change, the call stack is
+ continually growing.
+
+2008-09-16 - Time-based profiling and conversion to Lua 5.1
+ by Ben Wilhelm ( zorba-pepperfish@pavlovian.net ).
+ Added the ability to optionally choose profiling methods, along with a new
+ profiling method.
+
+Converted to Lua 5, a few improvements, and
+additional documentation by Tom Spilman ( tom@sickheadgames.com )
+
+Additional corrections and tidying by original author
+Daniel Silverstone ( dsilvers@pepperfish.net )
+
+== Status ==
+
+Daniel Silverstone is no longer using this code, and judging by how long it's
+been waiting for Lua 5.1 support, I don't think Tom Spilman is either. I'm
+perfectly willing to take on maintenance, so if you have problems or
+questions, go ahead and email me :)
+-- Ben Wilhelm ( zorba-pepperfish@pavlovian.net ) '
+
+== Copyright ==
+
+Lua profiler - Copyright Pepperfish 2002,2003,2004
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to
+do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+
+--]]
+
+
+--
+-- All profiler related stuff is stored in the top level table '_profiler'
+--
+_profiler = {}
+
+
+--
+-- newProfiler() creates a new profiler object for managing
+-- the profiler and storing state. Note that only one profiler
+-- object can be executing at one time.
+--
+function newProfiler(variant, sampledelay)
+ if _profiler.running then
+ print("Profiler already running.")
+ return
+ end
+
+ variant = variant or "time"
+
+ if variant ~= "time" and variant ~= "call" then
+ print("Profiler method must be 'time' or 'call'.")
+ return
+ end
+
+ local newprof = {}
+ for k,v in pairs(_profiler) do
+ newprof[k] = v
+ end
+ newprof.variant = variant
+ newprof.sampledelay = sampledelay or 100000
+ return newprof
+end
+
+
+--
+-- This function starts the profiler. It will do nothing
+-- if this (or any other) profiler is already running.
+--
+function _profiler.start(self)
+ if _profiler.running then
+ return
+ end
+ -- Start the profiler. This begins by setting up internal profiler state
+ _profiler.running = self
+ self.rawstats = {}
+ self.callstack = {}
+ if self.variant == "time" then
+ self.lastclock = os.clock()
+ debug.sethook( _profiler_hook_wrapper_by_time, "", self.sampledelay )
+ elseif self.variant == "call" then
+ debug.sethook( _profiler_hook_wrapper_by_call, "cr" )
+ else
+ print("Profiler method must be 'time' or 'call'.")
+ end
+end
+
+
+--
+-- This function stops the profiler. It will do nothing
+-- if a profiler is not running, and nothing if it isn't
+-- the currently running profiler.
+--
+function _profiler.stop(self)
+ if _profiler.running ~= self then
+ return
+ end
+ -- Stop the profiler.
+ debug.sethook( nil )
+ _profiler.running = nil
+end
+
+
+--
+-- Simple wrapper to handle the hook. You should not
+-- be calling this directly. Duplicated to reduce overhead.
+--
+function _profiler_hook_wrapper_by_call(action)
+ if _profiler.running == nil then
+ debug.sethook( nil )
+ end
+ _profiler.running:_internal_profile_by_call(action)
+end
+function _profiler_hook_wrapper_by_time(action)
+ if _profiler.running == nil then
+ debug.sethook( nil )
+ end
+ _profiler.running:_internal_profile_by_time(action)
+end
+
+
+--
+-- This is the main by-function-call function of the profiler and should not
+-- be called except by the hook wrapper
+--
+function _profiler._internal_profile_by_call(self,action)
+ -- Since we can obtain the 'function' for the item we've had call us, we
+ -- can use that...
+ local caller_info = debug.getinfo( 3 )
+ if caller_info == nil then
+ print "No caller_info"
+ return
+ end
+
+ if caller_info.short_src == "[C]" then
+ -- LMD -- These are builtin functions and for some reason the
+ -- debug hook does not catch the return of these functions.
+ -- So, for now, we are just going to skip them.
+ return
+ end
+
+ --SHG_LOG("[_profiler._internal_profile] "..(caller_info.name or "<nil>"))
+
+ -- Retrieve the most recent activation record...
+ local latest_ar = nil
+ if #self.callstack > 0 then
+ latest_ar = self.callstack[#self.callstack]
+ end
+
+ -- Are we allowed to profile this function?
+ local should_not_profile = 0
+ for k,v in pairs(self.prevented_functions) do
+ if k == caller_info.func then
+ should_not_profile = v
+ end
+ end
+ -- Also check the top activation record...
+ if latest_ar then
+ if latest_ar.should_not_profile == 2 then
+ should_not_profile = 2
+ end
+ end
+
+ -- Now then, are we in 'call' or 'return' ?
+ -- print("Profile:", caller_info.name, "SNP:", should_not_profile,
+ -- "Action:", action )
+ if action == "call" then
+ -- Making a call...
+ local this_ar = {}
+ this_ar.should_not_profile = should_not_profile
+ this_ar.parent_ar = latest_ar
+ this_ar.anon_child = 0
+ this_ar.name_child = 0
+ this_ar.children = {}
+ this_ar.children_time = {}
+ this_ar.clock_start = os.clock()
+ -- Last thing to do on a call is to insert this onto the ar stack...
+ table.insert( self.callstack, this_ar )
+ else
+ local this_ar = latest_ar
+ if this_ar == nil then
+ return -- No point in doing anything if no upper activation record
+ end
+
+ -- Right, calculate the time in this function...
+ this_ar.clock_end = os.clock()
+ this_ar.this_time = this_ar.clock_end - this_ar.clock_start
+
+ -- Now, if we have a parent, update its call info...
+ if this_ar.parent_ar then
+ this_ar.parent_ar.children[caller_info.func] =
+ (this_ar.parent_ar.children[caller_info.func] or 0) + 1
+ this_ar.parent_ar.children_time[caller_info.func] =
+ (this_ar.parent_ar.children_time[caller_info.func] or 0 ) +
+ this_ar.this_time
+ if caller_info.name == nil then
+ this_ar.parent_ar.anon_child =
+ this_ar.parent_ar.anon_child + this_ar.this_time
+ else
+ this_ar.parent_ar.name_child =
+ this_ar.parent_ar.name_child + this_ar.this_time
+ end
+ end
+ -- Now if we're meant to record information about ourselves, do so...
+ if this_ar.should_not_profile == 0 then
+ local inforec = self:_get_func_rec(caller_info.func,1)
+ inforec.count = inforec.count + 1
+ inforec.time = inforec.time + this_ar.this_time
+ inforec.anon_child_time = inforec.anon_child_time + this_ar.anon_child
+ inforec.name_child_time = inforec.name_child_time + this_ar.name_child
+ inforec.func_info = caller_info
+ for k,v in pairs(this_ar.children) do
+ inforec.children[k] = (inforec.children[k] or 0) + v
+ inforec.children_time[k] =
+ (inforec.children_time[k] or 0) + this_ar.children_time[k]
+ end
+ end
+
+ -- Last thing to do on return is to drop the last activation record...
+ table.remove( self.callstack, #self.callstack)
+ end
+end
+
+
+--
+-- This is the main by-time internal function of the profiler and should not
+-- be called except by the hook wrapper
+--
+function _profiler._internal_profile_by_time(self,action)
+ -- we do this first so we add the minimum amount of extra time to this call
+ local timetaken = os.clock() - self.lastclock
+
+ local depth = 3
+ local at_top = true
+ local last_caller
+ local caller = debug.getinfo(depth)
+ while caller do
+ if not caller.func then caller.func = "(tail call)" end
+ if self.prevented_functions[caller.func] == nil then
+ local info = self:_get_func_rec(caller.func, 1, caller)
+ info.count = info.count + 1
+ info.time = info.time + timetaken
+ if last_caller then
+ -- we're not the head, so update the "children" times also
+ if last_caller.name then
+ info.name_child_time = info.name_child_time + timetaken
+ else
+ info.anon_child_time = info.anon_child_time + timetaken
+ end
+ info.children[last_caller.func] =
+ (info.children[last_caller.func] or 0) + 1
+ info.children_time[last_caller.func] =
+ (info.children_time[last_caller.func] or 0) + timetaken
+ end
+ end
+ depth = depth + 1
+ last_caller = caller
+ caller = debug.getinfo(depth)
+ end
+
+ self.lastclock = os.clock()
+end
+
+
+--
+-- This returns a (possibly empty) function record for
+-- the specified function. It is for internal profiler use.
+--
+function _profiler._get_func_rec(self,func,force,info)
+ -- Find the function ref for 'func' (if force and not present, create one)
+ local ret = self.rawstats[func]
+ if ret == nil and force ~= 1 then
+ return nil
+ end
+ if ret == nil then
+ -- Build a new function statistics table
+ ret = {}
+ ret.func = func
+ ret.count = 0
+ ret.time = 0
+ ret.anon_child_time = 0
+ ret.name_child_time = 0
+ ret.children = {}
+ ret.children_time = {}
+ ret.func_info = info
+ self.rawstats[func] = ret
+ end
+ return ret
+end
+
+
+--
+-- This writes a profile report to the output file object. If
+-- sort_by_total_time is nil or false the output is sorted by
+-- the function time minus the time in it's children.
+--
+function _profiler.report( self, outfile, sort_by_total_time )
+
+ outfile:write
+ [[Lua Profile output created by profiler.lua. Copyright Pepperfish 2002+
+
+]]
+
+ -- This is pretty awful.
+ local terms = {}
+ if self.variant == "time" then
+ terms.capitalized = "Sample"
+ terms.single = "sample"
+ terms.pastverb = "sampled"
+ elseif self.variant == "call" then
+ terms.capitalized = "Call"
+ terms.single = "call"
+ terms.pastverb = "called"
+ else
+ assert(false)
+ end
+
+ local total_time = 0
+ local ordering = {}
+ for func,record in pairs(self.rawstats) do
+ table.insert(ordering, func)
+ end
+
+ if sort_by_total_time then
+ table.sort( ordering,
+ function(a,b) return self.rawstats[a].time > self.rawstats[b].time end
+ )
+ else
+ table.sort( ordering,
+ function(a,b)
+ local arec = self.rawstats[a]
+ local brec = self.rawstats[b]
+ local atime = arec.time - (arec.anon_child_time + arec.name_child_time)
+ local btime = brec.time - (brec.anon_child_time + brec.name_child_time)
+ return atime > btime
+ end
+ )
+ end
+
+ for i=1,#ordering do
+ local func = ordering[i]
+ local record = self.rawstats[func]
+ local thisfuncname = " " .. self:_pretty_name(func) .. " "
+ if string.len( thisfuncname ) < 42 then
+ thisfuncname =
+ string.rep( "-", (42 - string.len(thisfuncname))/2 ) .. thisfuncname
+ thisfuncname =
+ thisfuncname .. string.rep( "-", 42 - string.len(thisfuncname) )
+ end
+
+ total_time = total_time + ( record.time - ( record.anon_child_time +
+ record.name_child_time ) )
+ outfile:write( string.rep( "-", 19 ) .. thisfuncname ..
+ string.rep( "-", 19 ) .. "\n" )
+ outfile:write( terms.capitalized.." count: " ..
+ string.format( "%4d", record.count ) .. "\n" )
+ outfile:write( "Time spend total: " ..
+ string.format( "%4.3f", record.time ) .. "s\n" )
+ outfile:write( "Time spent in children: " ..
+ string.format("%4.3f",record.anon_child_time+record.name_child_time) ..
+ "s\n" )
+ local timeinself =
+ record.time - (record.anon_child_time + record.name_child_time)
+ outfile:write( "Time spent in self: " ..
+ string.format("%4.3f", timeinself) .. "s\n" )
+ outfile:write( "Time spent per " .. terms.single .. ": " ..
+ string.format("%4.5f", record.time/record.count) ..
+ "s/" .. terms.single .. "\n" )
+ outfile:write( "Time spent in self per "..terms.single..": " ..
+ string.format( "%4.5f", timeinself/record.count ) .. "s/" ..
+ terms.single.."\n" )
+
+ -- Report on each child in the form
+ -- Child <funcname> called n times and took a.bs
+ local added_blank = 0
+ for k,v in pairs(record.children) do
+ if self.prevented_functions[k] == nil or
+ self.prevented_functions[k] == 0
+ then
+ if added_blank == 0 then
+ outfile:write( "\n" ) -- extra separation line
+ added_blank = 1
+ end
+ outfile:write( "Child " .. self:_pretty_name(k) ..
+ string.rep( " ", 41-string.len(self:_pretty_name(k)) ) .. " " ..
+ terms.pastverb.." " .. string.format("%6d", v) )
+ outfile:write( " times. Took " ..
+ string.format("%4.2f", record.children_time[k] ) .. "s\n" )
+ end
+ end
+
+ outfile:write( "\n" ) -- extra separation line
+ outfile:flush()
+ end
+ outfile:write( "\n\n" )
+ outfile:write( "Total time spent in profiled functions: " ..
+ string.format("%5.3g",total_time) .. "s\n" )
+ outfile:write( [[
+
+END
+]] )
+ outfile:flush()
+end
+
+
+--
+-- This writes the profile to the output file object as
+-- loadable Lua source.
+--
+function _profiler.lua_report(self,outfile)
+ -- Purpose: Write out the entire raw state in a cross-referenceable form.
+ local ordering = {}
+ local functonum = {}
+ for func,record in pairs(self.rawstats) do
+ table.insert(ordering, func)
+ functonum[func] = #ordering
+ end
+
+ outfile:write(
+ "-- Profile generated by profiler.lua Copyright Pepperfish 2002+\n\n" )
+ outfile:write( "-- Function names\nfuncnames = {}\n" )
+ for i=1,#ordering do
+ local thisfunc = ordering[i]
+ outfile:write( "funcnames[" .. i .. "] = " ..
+ string.format("%q", self:_pretty_name(thisfunc)) .. "\n" )
+ end
+ outfile:write( "\n" )
+ outfile:write( "-- Function times\nfunctimes = {}\n" )
+ for i=1,#ordering do
+ local thisfunc = ordering[i]
+ local record = self.rawstats[thisfunc]
+ outfile:write( "functimes[" .. i .. "] = { " )
+ outfile:write( "tot=" .. record.time .. ", " )
+ outfile:write( "achild=" .. record.anon_child_time .. ", " )
+ outfile:write( "nchild=" .. record.name_child_time .. ", " )
+ outfile:write( "count=" .. record.count .. " }\n" )
+ end
+ outfile:write( "\n" )
+ outfile:write( "-- Child links\nchildren = {}\n" )
+ for i=1,#ordering do
+ local thisfunc = ordering[i]
+ local record = self.rawstats[thisfunc]
+ outfile:write( "children[" .. i .. "] = { " )
+ for k,v in pairs(record.children) do
+ if functonum[k] then -- non-recorded functions will be ignored now
+ outfile:write( functonum[k] .. ", " )
+ end
+ end
+ outfile:write( "}\n" )
+ end
+ outfile:write( "\n" )
+ outfile:write( "-- Child call counts\nchildcounts = {}\n" )
+ for i=1,#ordering do
+ local thisfunc = ordering[i]
+ local record = self.rawstats[thisfunc]
+ outfile:write( "children[" .. i .. "] = { " )
+ for k,v in record.children do
+ if functonum[k] then -- non-recorded functions will be ignored now
+ outfile:write( v .. ", " )
+ end
+ end
+ outfile:write( "}\n" )
+ end
+ outfile:write( "\n" )
+ outfile:write( "-- Child call time\nchildtimes = {}\n" )
+ for i=1,#ordering do
+ local thisfunc = ordering[i]
+ local record = self.rawstats[thisfunc];
+ outfile:write( "children[" .. i .. "] = { " )
+ for k,v in pairs(record.children) do
+ if functonum[k] then -- non-recorded functions will be ignored now
+ outfile:write( record.children_time[k] .. ", " )
+ end
+ end
+ outfile:write( "}\n" )
+ end
+ outfile:write( "\n\n-- That is all.\n\n" )
+ outfile:flush()
+end
+
+-- Internal function to calculate a pretty name for the profile output
+function _profiler._pretty_name(self,func)
+
+ -- Only the data collected during the actual
+ -- run seems to be correct.... why?
+ local info = self.rawstats[ func ].func_info
+ -- local info = debug.getinfo( func )
+
+ local name = ""
+ if info.what == "Lua" then
+ name = "L:"
+ end
+ if info.what == "C" then
+ name = "C:"
+ end
+ if info.what == "main" then
+ name = " :"
+ end
+
+ if info.name == nil then
+ name = name .. "<"..tostring(func) .. ">"
+ else
+ name = name .. info.name
+ end
+
+ if info.source then
+ name = name .. "@" .. info.source
+ else
+ if info.what == "C" then
+ name = name .. "@?"
+ else
+ name = name .. "@<string>"
+ end
+ end
+ name = name .. ":"
+ if info.what == "C" then
+ name = name .. "?"
+ else
+ name = name .. info.linedefined
+ end
+
+ return name
+end
+
+
+--
+-- This allows you to specify functions which you do
+-- not want profiled. Setting level to 1 keeps the
+-- function from being profiled. Setting level to 2
+-- keeps both the function and its children from
+-- being profiled.
+--
+-- BUG: 2 will probably act exactly like 1 in "time" mode.
+-- If anyone cares, let me (zorba) know and it can be fixed.
+--
+function _profiler.prevent(self, func, level)
+ self.prevented_functions[func] = (level or 1)
+end
+
+
+_profiler.prevented_functions = {
+ [_profiler.start] = 2,
+ [_profiler.stop] = 2,
+ [_profiler._internal_profile_by_time] = 2,
+ [_profiler._internal_profile_by_call] = 2,
+ [_profiler_hook_wrapper_by_time] = 2,
+ [_profiler_hook_wrapper_by_call] = 2,
+ [_profiler.prevent] = 2,
+ [_profiler._get_func_rec] = 2,
+ [_profiler.report] = 2,
+ [_profiler.lua_report] = 2,
+ [_profiler._pretty_name] = 2
+} \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/_lib/table.lua b/macros/luatex/latex/luacas/tex/_lib/table.lua
new file mode 100644
index 0000000000..70495d1479
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/_lib/table.lua
@@ -0,0 +1,185 @@
+-- Checks if two arrays are equal, starting at index i
+function ArrayEqual(a1, a2, i)
+ i = i or 1
+ while i <= math.max(#a1, #a2) do
+ if a1[i] ~= a2[i] then
+ return false
+ end
+ i = i + 1
+ end
+ return true
+end
+
+-- Checks if two arrays are equal, the first starting at index i, the second starting at index j
+function FancyArrayEqual(a1, a2, i, j)
+ i = i or 1
+ j = j or 1
+ while i <= math.max(#a1, #a2) or j <= math.max(#a1, #a2) do
+ if a1[i] ~= a2[j] then
+ return false
+ end
+ i = i + 1
+ j = j + 1
+ end
+ return true
+end
+
+-- Creates a copy of a table
+function Copy(orig)
+ local orig_type = type(orig)
+ local copy
+ if orig_type == 'table' then
+ copy = {}
+ for orig_key, orig_value in pairs(orig) do
+ copy[orig_key] = orig_value
+ end
+ copy = setmetatable(copy, getmetatable(orig))
+ else -- number, string, boolean, etc
+ copy = orig
+ end
+ return copy
+end
+
+-- Joins two arrays
+function JoinArrays(a1, a2)
+ local a = Copy(a1)
+ for index, value in ipairs(a2) do
+ a[index + #a1] = value
+ end
+ return a
+end
+
+-- Joins two arrays indexed from zero
+function JoinZeroArrays(a1, a2)
+ local a = Copy(a1)
+ a[#a1 + 1] = a2[0]
+ for index, value in ipairs(a2) do
+ a[index + #a1 + 1] = value
+ end
+ return a
+end
+
+-- Join two tables, using the second entry if a key appears in both tables
+function JoinTables(t1, t2)
+ local t = Copy(t1) or {}
+ for key, value in pairs(t2) do
+ t[key] = value
+ end
+ return t
+end
+
+-- Given an array, removes all occurances of that element from the array
+function Remove(a, e)
+ local r = Copy(a)
+ local found = false
+ for index, value in ipairs(r) do
+ if e == value then
+ found = true
+ end
+ if found then
+ r[index] = r[index + 1]
+ end
+ end
+ return r
+end
+
+-- Given an array, removes all elements in the second array from the first
+function RemoveAll(a1, a2)
+ local r = Copy(a1)
+ local removed = 0
+ for index, value in ipairs(r) do
+ if a2[value] then
+ removed = removed + 1
+ end
+ if removed then
+ r[index] = r[index + removed]
+ end
+ end
+ return r
+end
+
+-- Given an array of arrays, returns only the arrays that have no elements in common with the second array
+function RemoveAny(aa, a)
+ local toremove = {}
+ for _, v in ipairs(aa) do
+ for _, v1 in ipairs(v) do
+ for _, v2 in ipairs(a) do
+ if v1 == v2 then
+ toremove[#toremove+1] = v
+ goto endcheck
+ end
+ end
+ end
+ ::endcheck::
+ end
+ return RemoveAll(aa, toremove)
+end
+
+-- Converts an array to a string recursively
+function ToStringArray(t)
+ if string.sub(tostring(t), 1, 6) == "table:" then
+ local out = "{"
+ for index, value in ipairs(t) do
+ out = out .. ToStringArray(value)
+ if t[index + 1] then
+ out = out .. ", "
+ end
+ end
+ return out .. "}"
+ end
+ return tostring(t)
+end
+
+-- Converts a table to a string recursively
+function ToStringTable(t)
+ if string.sub(tostring(t), 1, 6) == "table:" then
+ local out = "{"
+ for index, value in pairs(t) do
+ out = out .. ToStringTable(index) .. " : " .. ToStringTable(value)
+ if t[index + 1] then
+ out = out .. ", "
+ end
+ end
+ return out .. "}"
+ end
+ return tostring(t)
+end
+
+-- Check if a table contains an element, and returns the index of that element if it does
+function Contains(t, e)
+ for index, value in pairs(t) do
+ if value == e then
+ return index
+ end
+ end
+ return false
+end
+
+-- Given an array a of unique elements, returns an array of the n-element subarrays of a
+function Subarrays(a, m)
+ local aout = {}
+ local l = 1
+ local newmax = {}
+
+ if(m <= 0) then
+ return {{}}, {0}
+ end
+
+ local rec, max = Subarrays(a, m - 1)
+
+ for recindex, set in pairs(rec) do
+ for index, element in pairs(a) do
+ if not set[element] and index > max[recindex] then
+ local new = Copy(set)
+ new[#new+1] = element
+ aout[l] = new
+ if not newmax[l] or index > newmax[l] then
+ newmax[l] = index
+ end
+ l = l + 1
+ end
+ end
+ end
+
+ return aout, newmax
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/_init.lua b/macros/luatex/latex/luacas/tex/algebra/_init.lua
new file mode 100644
index 0000000000..0a9b114cb9
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/_init.lua
@@ -0,0 +1,24 @@
+-- Loads algebra files in the correct order.
+require("_lib.table")
+
+require("core._init")
+
+require("algebra.ring")
+require("algebra.euclideandomain")
+require("algebra.field")
+require("algebra.polynomialring")
+require("algebra.integer")
+require("algebra.rational")
+require("algebra.integerquotientring")
+require("algebra.sqrtexpression")
+
+require("algebra.absexpression")
+require("algebra.equation")
+require("algebra.factorialexpression")
+require("algebra.logarithm")
+require("algebra.rootexpression")
+require("algebra.trigexpression")
+
+require("algebra.polynomialring.berlekampfactoring")
+require("algebra.polynomialring.zassenhausfactoring")
+require("algebra.polynomialring.decomposition") \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/absexpression.lua b/macros/luatex/latex/luacas/tex/algebra/absexpression.lua
new file mode 100644
index 0000000000..2bf0b6a7ef
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/absexpression.lua
@@ -0,0 +1,80 @@
+--- @class AbsExpression
+--- The absolute value of an expression.
+--- @field expression Expression
+AbsExpression = {}
+__AbsExpression = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new absolute value expression with the given expression.
+--- @param expression Expression
+--- @return AbsExpression
+function AbsExpression:new(expression)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ o.expression = expression
+
+ __o.__index = AbsExpression
+ __o.__tostring = function(a)
+ return '|' .. tostring(a.expression) .. '|'
+ end
+
+ o = setmetatable(o, __o)
+ return o
+end
+
+--- @return Expression
+function AbsExpression:evaluate()
+ if self.expression:isconstant() then
+ if self.expression >= Integer.zero() then
+ return self.expression
+ end
+ return -self.expression
+ end
+ return self
+end
+
+--- @return Expression
+function AbsExpression:autosimplify()
+ return AbsExpression(self.expression:autosimplify()):evaluate()
+end
+
+--- @return table<number, Expression>
+function AbsExpression:subexpressions()
+ return {self.expression}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return AbsExpression
+function AbsExpression:setsubexpressions(subexpressions)
+ return AbsExpression(subexpressions[1])
+end
+
+--- @param other Expression
+--- @return boolean
+function AbsExpression:order(other)
+ return FunctionExpression("abs", self.expression):order(other)
+end
+
+--- @return string
+function AbsExpression:tolatex()
+ return "\\left|" .. self.expression:tolatex() .. "\\right|"
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__AbsExpression.__index = CompoundExpression
+__AbsExpression.__call = AbsExpression.new
+AbsExpression = setmetatable(AbsExpression, __AbsExpression)
+
+----------------------
+-- Static constants --
+----------------------
+ABS = function (a)
+ return AbsExpression(a)
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/equation.lua b/macros/luatex/latex/luacas/tex/algebra/equation.lua
new file mode 100644
index 0000000000..4e81d5ca95
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/equation.lua
@@ -0,0 +1,183 @@
+--- @class Equation
+--- An expression that represents an equation of the form lhs = rhs.
+--- @field lhs Expression
+--- @field rhs Expression
+Equation = {}
+__Equation = {}
+
+--------------------------
+-- Static functionality --
+--------------------------
+
+--- Attempts to isolate the variable var in lhs by moving expressions to rhs. Ony performs a single step.
+--- @param lhs Expression
+--- @param rhs Expression
+--- @param var SymbolExpression
+--- @return Expression, Expression
+function Equation.isolatelhs(lhs, rhs, var)
+ if lhs:type() == BinaryOperation then
+ local stay = Integer.zero()
+ local switch = Integer.zero()
+ if lhs.operation == BinaryOperation.ADD then
+ for _, exp in ipairs(lhs:subexpressions()) do
+ if exp:freeof(var) then
+ switch = switch + exp
+ else
+ stay = stay + exp
+ end
+ end
+ if switch == Integer.zero() then
+ lhs = lhs:factor() -- TODO: Replace with collect for efficiency reasons
+ else
+ return stay:autosimplify(), (rhs - switch):autosimplify()
+ end
+ end
+ if lhs.operation == BinaryOperation.MUL then
+ stay = Integer.one()
+ switch = Integer.one()
+ for _, exp in ipairs(lhs:subexpressions()) do
+ if exp:freeof(var) then
+ switch = switch * exp
+ else
+ stay = stay * exp
+ end
+ end
+ return stay:autosimplify(), (rhs / switch):autosimplify()
+ end
+ if lhs.operation == BinaryOperation.POW then
+ if lhs:subexpressions()[1]:freeof(var) then
+ return lhs:subexpressions()[2]:autosimplify(), Logarithm(lhs:subexpressions()[1], rhs):autosimplify()
+ elseif lhs:subexpressions()[2]:freeof(var) then
+ return lhs:subexpressions()[1]:autosimplify(), (rhs ^ (Integer.one()/lhs:subexpressions()[2])):autosimplify()
+ end
+ end
+ elseif lhs:type() == Logarithm then
+ if lhs.base:freeof(var) then
+ return lhs.expression:autosimplify(), (lhs.base ^ rhs):autosimplify()
+ elseif lhs.expression:freeof(var) then
+ return lhs.base:autosimplify(), (lhs.expression ^ (Integer.one()/rhs)):autosimplify()
+ end
+ elseif lhs:type() == TrigExpression then
+ return lhs.expression:autosimplify(), TrigExpression(TrigExpression.INVERSES[lhs.name], rhs):autosimplify()
+ end
+
+ return lhs, rhs
+end
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new equation with the given expressions.
+--- @param lhs Expression
+--- @param rhs Expression
+--- @return Equation
+function Equation:new(lhs, rhs)
+
+ if lhs:type() == Equation or rhs:type() == Equation then
+ error("Sent parameter of wrong type: cannot nest equations or inequalities")
+ end
+
+ local o = {}
+ local __o = Copy(__ExpressionOperations) -- TODO: Ensure only one metatable for each instance of a class
+
+ o.lhs = lhs
+ o.rhs = rhs
+
+ __o.__index = Equation
+ __o.__tostring = function(a)
+ return tostring(a.lhs) .. ' = ' .. tostring(a.rhs)
+ end
+ __o.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway
+ if not b:type() == Equation then
+ return false
+ end
+ return a.lhs == b.lhs and a.rhs == b.rhs
+ end
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- Evaluation in this case just checks for structural equality, or guarenteed inequality in the case of constants
+--- @return Equation|boolean
+function Equation:evaluate()
+ if self.lhs == self.rhs then
+ return true -- TODO: Add Boolean Expressions
+ end
+ if self.lhs:isconstant() and self.rhs:isconstant() and self.lhs ~= self.rhs then
+ return false
+ end
+ return self
+end
+
+--- @return Equation|boolean
+function Equation:autosimplify()
+ local lhs = self.lhs:autosimplify()
+ local rhs = self.rhs:autosimplify()
+
+ return Equation(lhs, rhs):evaluate()
+end
+
+--- @return table<number, Expression>
+function Equation:subexpressions()
+ return {self.lhs, self.rhs}
+end
+
+--- Attempts to solve the equation for a particular variable.
+--- @param var SymbolExpression
+--- @return Equation
+function Equation:solvefor(var)
+ local lhs = self.lhs
+ local rhs = self.rhs
+
+ if lhs:freeof(var) and rhs:freeof(var) then
+ return self
+ end
+
+ -- Check for monovariate polynomial expressions
+ local root = (lhs - rhs):autosimplify()
+ local poly, status = root:expand():topolynomial()
+ if status then
+ -- TODO: Add Set expressions
+ return Equation(var, poly:roots()[1])
+ end
+
+ local newlhs, newrhs = root, Integer(0)
+ local oldlhs
+ while newlhs ~= var and oldlhs ~= newlhs do
+ oldlhs = newlhs
+ newlhs, newrhs = Equation.isolatelhs(newlhs, newrhs, var)
+ end
+
+ return Equation(newlhs, newrhs)
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return Equation
+function Equation:setsubexpressions(subexpressions)
+ return Equation(subexpressions[1], subexpressions[2])
+end
+
+--- @param other Expression
+--- @return boolean
+function Equation:order(other)
+ if other:isatomic() then
+ return false
+ end
+
+ return self.lhs:order(other)
+end
+
+--- @return string
+function Equation:tolatex()
+ return self.lhs:tolatex() .. '=' .. self.rhs:tolatex()
+end
+
+-----------------
+-- Inheritance --
+-----------------
+__Equation.__index = CompoundExpression
+__Equation.__call = Equation.new
+Equation = setmetatable(Equation, __Equation) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/euclideandomain.lua b/macros/luatex/latex/luacas/tex/algebra/euclideandomain.lua
new file mode 100644
index 0000000000..fab2c5c7c2
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/euclideandomain.lua
@@ -0,0 +1,63 @@
+--- @class EuclideanDomain
+--- Interface for an element of a euclidean domain.
+EuclideanDomain = {}
+__EuclideanDomain = {}
+
+----------------------
+-- Required methods --
+----------------------
+
+--- @param b EuclideanDomain
+--- @return EuclideanDomain, EuclideanDomain
+function EuclideanDomain:divremainder(b)
+ error("Called unimplemented method : divremainder()")
+end
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- @return boolean
+function EuclideanDomain:iscommutative()
+ return true
+end
+
+--------------------------
+-- Instance metamethods --
+--------------------------
+
+__EuclideanOperations = Copy(__RingOperations)
+
+-- Division with remainder
+-- Unfortunately, this can only return 1 result, so it returns the quotient - for the remainder use a % b, or a:divremainder(b)
+__EuclideanOperations.__idiv = function(a, b)
+ if(b == b:zero()) then
+ error("Cannot divide by zero.")
+ end
+ local aring, bring = a:getring(), b:getring()
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to divide two elements of incompatable rings")
+ end
+ return a:inring(oring):divremainder(b:inring(oring))
+end
+
+__EuclideanOperations.__mod = function(a, b)
+ if(b == b:zero()) then
+ error("Cannot divide by zero.")
+ end
+ local aring, bring = a:getring(), b:getring()
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to divide two elements of incompatable rings")
+ end
+ local _,q = a:inring(oring):divremainder(b:inring(oring))
+ return q
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__EuclideanDomain.__index = Ring
+EuclideanDomain = setmetatable(EuclideanDomain, __EuclideanDomain) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/factorialexpression.lua b/macros/luatex/latex/luacas/tex/algebra/factorialexpression.lua
new file mode 100644
index 0000000000..1c5b824ff7
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/factorialexpression.lua
@@ -0,0 +1,102 @@
+--- @class FactorialExpression
+--- The factorial of an expression.
+--- @field expression Expression
+FactorialExpression = {}
+__FactorialExpression = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new factorial expression with the given expression.
+--- @param expression Expression
+--- @return FactorialExpression
+function FactorialExpression:new(expression)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ o.expression = expression
+
+ __o.__index = FactorialExpression
+ __o.__tostring = function(a)
+ return '(' .. tostring(a.expression) .. ')!'
+ end
+
+ o = setmetatable(o, __o)
+ return o
+end
+
+--- @return Expression
+function FactorialExpression:evaluate()
+ if self.expression:type() == Integer then
+ if self.expression < Integer.zero() then
+ error("Aritmetic Error: Factorials of negative integers are not defined.")
+ end
+
+ if not FactorialExpression.LIMIT then
+ FactorialExpression.LIMIT = Integer(5000)
+ end
+
+ if self.expression > FactorialExpression.LIMIT then
+ return self
+ end
+ -- TODO: More efficient factorial computations.
+ local out = Integer.one()
+ local i = Integer.zero()
+ while i < self.expression do
+ i = i + Integer.one()
+ out = out * i
+ end
+ return out
+ end
+ return self
+end
+
+--- @return Expression
+function FactorialExpression:autosimplify()
+ return FactorialExpression(self.expression:autosimplify()):evaluate()
+end
+
+--- @return table<number, Expression>
+function FactorialExpression:subexpressions()
+ return {self.expression}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return AbsExpression
+function FactorialExpression:setsubexpressions(subexpressions)
+ return FactorialExpression(subexpressions[1])
+end
+
+--- @param other Expression
+--- @return boolean
+function FactorialExpression:order(other)
+ return FunctionExpression("fact", self.expression):order(other)
+end
+
+--- @return string
+function FactorialExpression:tolatex()
+ if self.expression:isatomic() then
+ return self.expression:tolatex() .. "!"
+ end
+ return "(" .. self.expression:tolatex() .. ")!"
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__FactorialExpression.__index = CompoundExpression
+__FactorialExpression.__call = FactorialExpression.new
+FactorialExpression = setmetatable(FactorialExpression, __FactorialExpression)
+
+----------------------
+-- Static constants --
+----------------------
+
+-- Do not attempt to compute factorials larger than this.
+FactorialExpression.LIMIT = Integer(5000)
+
+FACT = function (a)
+ return FactorialExpression(a)
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/field.lua b/macros/luatex/latex/luacas/tex/algebra/field.lua
new file mode 100644
index 0000000000..c845e94c55
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/field.lua
@@ -0,0 +1,69 @@
+--- @class Field
+--- Interface for an element of a field.
+Field = {}
+__Field = {}
+
+----------------------
+-- Required methods --
+----------------------
+
+--- @return Field
+function Field:div(b)
+ return self:mul(b:inv())
+end
+
+--- @return Field
+function Field:inv()
+ error("Called unimplemented method: inv()")
+end
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Field exponentiation based on the definition. Specific rings may implement more efficient methods.
+--- @return Field
+function Field:pow(n)
+ local base = self
+ if(n < Integer.zero()) then
+ n = -n
+ base = base:inv()
+ end
+ local k = Integer.zero()
+ local b = self.getring().one()
+ while k < n do
+ b = b.mul(base)
+ k = k + Integer.one()
+ end
+ return b
+end
+
+--------------------------
+-- Instance metamethods --
+--------------------------
+
+__FieldOperations = Copy(__EuclideanOperations)
+
+__FieldOperations.__div = function(a, b)
+ if not b.getring and not b:isconstant() then
+ return BinaryOperation.DIVEXP({a, b})
+ end
+
+ if(b == b:zero()) then
+ error("Arithmetic Error: Cannot divide by zero.")
+ end
+
+ local aring, bring = a:getring(), b:getring()
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to divide two elements of incompatable rings")
+ end
+ return a:inring(oring):div(b:inring(oring))
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__Field.__index = EuclideanDomain
+Field = setmetatable(Field, __Field) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/integer.lua b/macros/luatex/latex/luacas/tex/algebra/integer.lua
new file mode 100644
index 0000000000..a3c54f498c
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/integer.lua
@@ -0,0 +1,952 @@
+--- @class Integer
+--- Represents an element of the ring of integers.
+--- @field self table<number, number>
+--- @field sign number
+Integer = {}
+__Integer = {}
+
+--------------------------
+-- Static functionality --
+--------------------------
+
+-- The length of each digit in base 10. 10^15 < 2^53 < 10^16, so 15 is the highest value that will work with double-percision numbers.
+-- For multiplication to work properly, however, this also must be even so we can take the square root of the digit size exactly.
+-- 10^14 is still larger than 2^26, so it is still efficient to do multiplication this way.
+Integer.DIGITLENGTH = 14
+-- The maximum size for a digit. While this doesn't need to be a power of 10, it makes implementing converting to and from strings much easier.
+Integer.DIGITSIZE = 10 ^ Integer.DIGITLENGTH
+-- Partition size for multiplying integers so we can get both the upper and lower bits of each digits
+Integer.PARTITIONSIZE = math.floor(math.sqrt(Integer.DIGITSIZE))
+
+--- Method for computing the gcd of two integers using Euclid's algorithm.
+--- @param a Integer
+--- @param b Integer
+--- @return Integer
+function Integer.gcd(a, b)
+ while b ~= Integer.zero() do
+ a, b = b, a%b
+ end
+ return a
+end
+
+--- Method for computing the gcd of two integers using Euclid's algorithm.
+--- Also returns Bezout's coefficients via extended gcd.
+--- @param a Integer
+--- @param b Integer
+--- @return Integer, Integer, Integer
+function Integer.extendedgcd(a, b)
+ local oldr, r = a, b
+ local olds, s = Integer.one(), Integer.zero()
+ local oldt, t = Integer.zero(), Integer.one()
+ while r ~= Integer.zero() do
+ local q = oldr // r
+ oldr, r = r, oldr - q*r
+ olds, s = s, olds - q*s
+ oldt, t = t, oldt - q*t
+ end
+ return oldr, olds, oldt
+end
+
+--- Method for computing the larger of two integers.
+--- Also returns the other integer for sorting purposes.
+--- @param a Integer
+--- @param b Integer
+--- @return Integer, Integer
+function Integer.max(a, b)
+ if a > b then
+ return a, b
+ end
+ return b, a
+end
+
+--- Method for computing the smaller of two integers.
+--- Also returns the other integer for sorting purposes.
+--- @param a Integer
+--- @param b Integer
+--- @return Integer, Integer
+function Integer.min(a, b)
+ if a < b then
+ return a, b
+ end
+ return b, a
+end
+
+--- Methods for computing the larger magnitude of two integers.
+--- Also returns the other integer for sorting purposes, and the number -1 if the two values were swapped, 1 if not.
+--- @param a Integer
+--- @param b Integer
+--- @return Integer, Integer, number
+function Integer.absmax(a, b)
+ if b:ltabs(a) then
+ return a, b, 1
+ end
+ return b, a, -1
+end
+
+-- Returns the ceiling of the log base (defaults to 10) of a.
+-- In other words, returns the least n such that base^n > a.
+--- @param a Integer
+--- @param base Integer
+--- @return Integer
+function Integer.ceillog(a, base)
+ base = base or Integer(10)
+ local k = Integer.zero()
+
+ while (base ^ k) < a do
+ k = k + Integer.one()
+ end
+
+ return k
+end
+
+--- Returns a ^ b (mod n). This should be used when a ^ b is potentially large.
+--- @param a Integer
+--- @param b Integer
+--- @param n Integer
+--- @return Integer
+function Integer.powmod(a, b, n)
+ if n == Integer.one() then
+ return Integer.zero()
+ else
+ local r = Integer.one()
+ a = a % n
+ while b > Integer.zero() do
+ if b % Integer(2) == Integer.one() then
+ r = (r * a) % n
+ end
+ a = (a ^ Integer(2)) % n
+ b = b // Integer(2)
+ end
+ return r
+ end
+end
+
+--- @return RingIdentifier
+local t = {ring=Integer}
+t = setmetatable(t, {__index = Integer, __eq = function(a, b)
+ return a["ring"] == b["ring"]
+end, __tostring = function(a)
+ return "ZZ"
+end})
+function Integer.makering()
+ return t
+end
+
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+-- So we don't have to copy the Euclidean operations each time we create an integer.
+local __o = Copy(__EuclideanOperations)
+__o.__index = Integer
+__o.__tostring = function(a) -- Only works if the digit size is a power of 10
+ local out = ""
+ for i, digit in ipairs(a) do
+ local pre = tostring(math.floor(digit))
+ if i ~= #a then
+ while #pre ~= Integer.DIGITLENGTH do
+ pre = "0" .. pre
+ end
+ end
+ out = pre .. out
+ end
+ if a.sign == -1 then
+ out = "-" .. out
+ end
+ return out
+end
+__o.__div = function(a, b) -- Constructor for a rational number disguised as division
+ if not b.getring then
+ return BinaryOperation.DIVEXP({a, b})
+ end
+ if(a:getring() == Integer:getring() and b:getring() == Integer:getring()) then
+ return Rational(a, b)
+ end
+ return __FieldOperations.__div(a, b)
+end
+__o.__concat = function(a, b) -- Like a decimal, but fancier. Used mainly for the parser with decimal numbers.
+ return a + b / (Integer(10) ^ Integer.ceillog(b))
+end
+
+--- Creates a new integer given a string or number representation of the integer.
+--- @param n number|string|Integer
+--- @return Integer
+function Integer:new(n)
+ local o = {}
+ o = setmetatable(o, __o)
+
+ if not n then
+ o[1] = 0
+ o.sign = 0
+ return o
+ end
+
+ -- Can convert any floating-point number into an integer, though we generally only want to pass whole numbers into this.
+ -- This will only approximate very large floating point numbers to a small proportion of the total significant digits
+ -- After that the result will just be nonsense - strings should probably be used for big numbers
+ if type(n) == "number" then
+ n = math.floor(n)
+ if n == 0 then
+ o[1] = 0
+ o.sign = 0
+ else
+ if n < 0 then
+ n = -n
+ o.sign = -1
+ else
+ o.sign = 1
+ end
+ local i = 1
+ while n >= Integer.DIGITSIZE do
+ o[i] = n % Integer.DIGITSIZE
+ n = n // Integer.DIGITSIZE
+ i = i + 1
+ end
+ o[i] = n
+ end
+ -- Only works on strings that are exact (signed) integers
+ elseif type(n) == "string" then
+ if not tonumber(n) then
+ error("Sent parameter of wrong type: " .. n .. " is not an integer.")
+ end
+ if n == "0" then
+ o[1] = 0
+ o.sign = 0
+ else
+ local s = 1
+ if string.sub(n, 1, 1) == "-" then
+ s = s + 1
+ o.sign = -1
+ else
+ o.sign = 1
+ end
+
+ while string.sub(n, s, s) == "0" do
+ s = s + 1
+ end
+
+ local e = #n
+ local i = 1
+ while e > s + Integer.DIGITLENGTH - 1 do
+ o[i] = tonumber(string.sub(n, e - Integer.DIGITLENGTH + 1, e))
+ e = e - Integer.DIGITLENGTH
+ i = i + 1
+ end
+ o[i] = tonumber(string.sub(n, s, e)) or 0
+ end
+ -- Copying is expensive in Lua, so this constructor probably should only sparsely be called with an Integer argument.
+ elseif type(n) == "table" then
+ o = Copy(n)
+ else
+ error("Sent parameter of wrong type: Integer does not accept " .. type(n) .. ".")
+ end
+
+ return o
+end
+
+--- Returns the ring this object is an element of.
+--- @return RingIdentifier
+function Integer:getring()
+ return t
+end
+
+--- @param ring RingIdentifier
+--- @return Ring
+function Integer:inring(ring)
+ if ring == self:getring() then
+ return self
+ end
+
+ if ring == PolynomialRing:getring() then
+ return PolynomialRing({self:inring(ring.child)}, ring.symbol)
+ end
+
+ if ring == Rational:getring() then
+ if ring.child then
+ return Rational(self:inring(ring.child), self:inring(ring.child):one(), true)
+ end
+ return Rational(self, Integer.one(), true):inring(ring)
+ end
+
+ if ring == IntegerModN:getring() then
+ return IntegerModN(self, ring.modulus)
+ end
+
+ error("Unable to convert element to proper ring.")
+end
+
+--- @param b Integer
+--- @return Integer
+function Integer:add(b)
+ if self.sign == 1 and b.sign == -1 then
+ return self:usub(b, 1)
+ end
+ if self.sign == -1 and b.sign == 1 then
+ return self:usub(b, -1)
+ end
+
+ local sign = self.sign
+ if sign == 0 then
+ sign = b.sign
+ end
+ return self:uadd(b, sign)
+end
+
+--- Addition without sign so we don't have to create an entire new integer when switching signs.
+--- @param b Integer
+--- @param sign number
+--- @return Integer
+function Integer:uadd(b, sign)
+ local o = Integer()
+ o.sign = sign
+
+ local c = 0
+ local n = math.max(#self, #b)
+ for i = 1, n do
+ local s = (self[i] or 0) + (b[i] or 0) + c
+ if s >= Integer.DIGITSIZE then
+ o[i] = s - Integer.DIGITSIZE
+ c = 1
+ else
+ o[i] = s
+ c = 0
+ end
+ end
+ if c == 1 then
+ o[n + 1] = c
+ end
+ return o
+end
+
+--- @param b Integer
+--- @return Integer
+function Integer:sub(b)
+ if self.sign == 1 and b.sign == -1 then
+ return self:uadd(b, 1)
+ end
+ if self.sign == -1 and b.sign == 1 then
+ return self:uadd(b, -1)
+ end
+
+ local sign = self.sign
+ if sign == 0 then
+ sign = b.sign
+ end
+ return self:usub(b, sign)
+end
+
+-- Subtraction without sign so we don't have to create an entire new integer when switching signs.
+-- Uses subtraction by compliments.
+--- @param b Integer
+--- @param sign number
+--- @return Integer
+function Integer:usub(b, sign)
+ local a, b, swap = Integer.absmax(self, b)
+ local o = Integer()
+ o.sign = sign * swap
+
+ local c = 0
+ local n = #a
+ for i = 1, n do
+ local s = (a[i] or 0) + Integer.DIGITSIZE - 1 - (b[i] or 0) + c
+ if i == 1 then
+ s = s + 1
+ end
+ if s >= Integer.DIGITSIZE then
+ o[i] = s - Integer.DIGITSIZE
+ c = 1
+ else
+ o[i] = s
+ c = 0
+ end
+ end
+
+ -- Remove leading zero digits, since we want integer representations to be unique.
+ while o[n] == 0 do
+ o[n] = nil
+ n = n - 1
+ end
+
+ if not o[1] then
+ o[1] = 0
+ o.sign = 0
+ end
+
+ return o
+end
+
+--- @return Integer
+function Integer:neg()
+ local o = Integer()
+ o.sign = -self.sign
+ for i, digit in ipairs(self) do
+ o[i] = digit
+ end
+ return o
+end
+
+--- @param b Integer
+--- @return Integer
+function Integer:mul(b)
+ local o = Integer()
+ o.sign = self.sign * b.sign
+ if o.sign == 0 then
+ o[1] = 0
+ return o
+ end
+
+ -- Fast single-digit multiplication in the most common case
+ if #self == 1 and #b == 1 then
+ o[2], o[1] = self:mulone(self[1], b[1])
+
+ if o[2] == 0 then
+ o[2] = nil
+ end
+
+ return o
+ end
+
+ -- "Grade school" multiplication algorithm for numbers with small numbers of digits works faster than Karatsuba
+ local n = #self
+ local m = #b
+ o[1] = 0
+ o[2] = 0
+ for i = 2, n+m do
+ o[i + 1] = 0
+ for j = math.max(1, i-m), math.min(n, i-1) do
+ local u, l = self:mulone(self[j], b[i - j])
+ o[i - 1] = o[i - 1] + l
+ o[i] = o[i] + u
+ if o[i - 1] >= Integer.DIGITSIZE then
+ o[i - 1] = o[i - 1] - Integer.DIGITSIZE
+ o[i] = o[i] + 1
+ end
+ if o[i] >= Integer.DIGITSIZE then
+ o[i] = o[i] - Integer.DIGITSIZE
+ o[i + 1] = o[i + 1] + 1
+ end
+ end
+ end
+
+ -- Remove leading zero digits, since we want integer representations to be unique.
+ if o[n+m+1] == 0 then
+ o[n+m+1] = nil
+ end
+
+ if o[n+m] == 0 then
+ o[n+m] = nil
+ end
+
+ return o
+end
+
+--- Multiplies two single-digit numbers and returns two digits.
+--- @param a number
+--- @param b number
+--- @return number, number
+function Integer:mulone(a, b)
+ local P = Integer.PARTITIONSIZE
+
+ local a1 = a // P
+ local a2 = a % P
+ local b1 = b // P
+ local b2 = b % P
+
+ local u = a1 * b1
+ local l = a2 * b2
+
+ local m = ((a1 * b2) + (b1 * a2))
+ local mu = m // P
+ local ml = m % P
+
+ u = u + mu
+ l = l + ml * P
+
+ if l >= Integer.DIGITSIZE then
+ l = l - Integer.DIGITSIZE
+ u = u + 1
+ end
+
+ return u, l
+end
+
+--- Naive exponentiation is slow even for small exponents, so this uses binary exponentiation.
+--- @param b Integer
+--- @return Integer
+function Integer:pow(b)
+ if b < Integer.zero() then
+ return Integer.one() / (self ^ -b)
+ end
+
+ if b == Integer.zero() then
+ return Integer.one()
+ end
+
+ -- Fast single-digit exponentiation
+ if #self == 1 and #b == 1 then
+ local test = (self.sign * self[1]) ^ b[1]
+ if test < Integer.DIGITSIZE and test > -Integer.DIGITSIZE then
+ return Integer(test)
+ end
+ end
+
+ local x = self
+ local y = Integer.one()
+ while b > Integer.one() do
+ if b[1] % 2 == 0 then
+ x = x * x
+ b = b:divbytwo()
+ else
+ y = x * y
+ x = x * x
+ b = b:divbytwo()
+ end
+ end
+
+ return x * y
+end
+
+-- Fast integer division by two for binary exponentiation.
+--- @return Integer
+function Integer:divbytwo()
+ local o = Integer()
+ o.sign = self.sign
+ for i = #self, 1, -1 do
+ if self[i] % 2 == 0 then
+ o[i] = self[i] // 2
+ else
+ o[i] = self[i] // 2
+ if i ~= 1 then
+ o[i - 1] = self[i - 1] * 2
+ end
+ end
+ end
+ return o
+end
+
+--- Division with remainder over the integers. Uses the standard base 10 long division algorithm.
+--- @param b Integer
+--- @return Integer, Integer
+function Integer:divremainder(b)
+ if self >= Integer.zero() and b > self or self <= Integer.zero() and b < self then
+ return Integer.zero(), Integer(self)
+ end
+
+ if #self == 1 and #b == 1 then
+ return Integer((self[1]*self.sign) // (b[1]*b.sign)), Integer((self[1]*self.sign) % (b[1]*b.sign))
+ end
+
+ local Q = Integer()
+ local R = Integer()
+
+ Q.sign = self.sign * b.sign
+ R.sign = 1
+ local negativemod = false
+ if b.sign == -1 then
+ b.sign = -b.sign
+ negativemod = true
+ end
+
+ for i = #self, 1, -1 do
+ local s = tostring(math.floor(self[i]))
+ while i ~= #self and #s ~= Integer.DIGITLENGTH do
+ s = "0" .. s
+ end
+ Q[i] = 0
+ for j = 1, #s do
+ R = R:mulbyten()
+ R[1] = R[1] + tonumber(string.sub(s, j, j))
+ if R[1] > 0 then
+ R.sign = 1
+ end
+ while R >= b do
+ R = R - b
+ Q[i] = Q[i] + 10^(#s - j)
+ end
+ end
+ end
+
+ -- Remove leading zero digits, since we want integer representations to be unique.
+ while Q[#Q] == 0 do
+ Q[#Q] = nil
+ end
+
+ if negativemod then
+ R = -R
+ b.sign = -b.sign
+ elseif self.sign == -1 then
+ R = b - R
+ end
+
+ return Q, R
+end
+
+--- Fast in-place multiplication by ten for the division algorithm. This means the number IS MODIFIED by this method unlike the rest of the library.
+--- @return Integer
+function Integer:mulbyten()
+ local DIGITSIZE = Integer.DIGITSIZE
+ for i, _ in ipairs(self) do
+ self[i] = self[i] * 10
+ end
+ for i, _ in ipairs(self) do
+ if self[i] > DIGITSIZE then
+ local msd = self[i] // DIGITSIZE
+ if self[i+1] then
+ self[i+1] = self[i+1] + msd
+ else
+ self[i+1] = msd
+ end
+ self[i] = self[i] - DIGITSIZE*msd
+ end
+ end
+ return self
+end
+
+--- @param b Integer
+--- @return boolean
+function Integer:eq(b)
+ for i, digit in ipairs(self) do
+ if not b[i] or not (b[i] == digit) then
+ return false
+ end
+ end
+ return #self == #b and self.sign == b.sign
+end
+
+--- @param b Integer
+--- @return boolean
+function Integer:lt(b)
+ local selfsize = #self
+ local bsize = #b
+ if selfsize < bsize then
+ return b.sign == 1
+ end
+ if selfsize > bsize then
+ return self.sign == -1
+ end
+ local n = selfsize
+ while n > 0 do
+ if self[n]*self.sign < b[n]*b.sign then
+ return true
+ end
+ if self[n]*self.sign > b[n]*b.sign then
+ return false
+ end
+ n = n - 1
+ end
+ return false
+end
+
+--- Same as less than, but ignores signs.
+--- @param b Integer
+--- @return boolean
+function Integer:ltabs(b)
+ if #self < #b then
+ return true
+ end
+ if #self > #b then
+ return false
+ end
+ local n = #self
+ while n > 0 do
+ if self[n] < b[n] then
+ return true
+ end
+ if self[n] > b[n] then
+ return false
+ end
+ n = n - 1
+ end
+ return false
+end
+
+--- @param b Integer
+--- @return boolean
+function Integer:le(b)
+ local selfsize = #self
+ local bsize = #b
+ if selfsize < bsize then
+ return b.sign == 1
+ end
+ if selfsize > bsize then
+ return self.sign == -1
+ end
+ local n = selfsize
+ while n > 0 do
+ if self[n]*self.sign < b[n]*b.sign then
+ return true
+ end
+ if self[n]*self.sign > b[n]*b.sign then
+ return false
+ end
+ n = n - 1
+ end
+ return true
+end
+
+local zero = Integer:new(0)
+--- @return Integer
+function Integer:zero()
+ return zero
+end
+
+local one = Integer:new(1)
+--- @return Integer
+function Integer:one()
+ return one
+end
+
+--- Returns this integer as a floating point number. Can only approximate the value of large integers.
+--- @return number
+function Integer:asnumber()
+ local n = 0
+ for i, digit in ipairs(self) do
+ n = n + digit * Integer.DIGITSIZE ^ (i - 1)
+ end
+ return self.sign*math.floor(n)
+end
+
+--- Returns all positive divisors of the integer. Not guarenteed to be in any order.
+--- @return table<number, Integer>
+function Integer:divisors()
+ local primefactors = self:primefactorizationrec()
+ local divisors = {}
+
+ local terms = {}
+ for prime in pairs(primefactors) do
+ if prime == Integer(-1) then
+ primefactors[prime] = nil
+ end
+ terms[prime] = Integer.zero()
+ end
+
+ local divisor = Integer.one()
+
+ while true do
+ divisors[#divisors+1] = divisor
+ for prime, power in pairs(primefactors) do
+ if terms[prime] < power then
+ terms[prime] = terms[prime] + Integer.one()
+ divisor = divisor * prime
+ break
+ else
+ terms[prime] = Integer.zero()
+ divisor = divisor / (prime ^ power)
+ end
+ end
+ if divisor == Integer.one() then
+ break
+ end
+ end
+
+ return divisors
+end
+
+--- Returns whether this integer is a prime power, of the form p^a for prime p and positive integer a.
+--- If it is a prime power, also returns the prime and the power.
+--- @return boolean, Expression|nil, Expression|nil
+function Integer:isprimepower()
+ if self <= Integer.one() then
+ return false
+ end
+ local factorization = self:primefactorization()
+ if factorization:type() == BinaryOperation and #factorization:subexpressions() == 1 then
+ return true, factorization.expressions[1].expressions[2], factorization.expressions[1].expressions[1]
+ end
+ return false
+end
+
+--- Returns whether this integer is a perfect power, of the form a^b for positive integers a and b.
+--- If it is a prime power, also returns the prime and the power.
+--- @return boolean, Expression|nil, Expression|nil
+function Integer:isperfectpower()
+ if self <= Integer.one() then
+ return false
+ end
+ local factorization = self:primefactorization()
+ if factorization:type() ~= BinaryOperation then
+ return false
+ end
+ local power = Integer.zero()
+ for _, term in ipairs(factorization:subexpressions()) do
+ power = Integer.gcd(power, term.expressions[2])
+ if power == Integer.one() then
+ return false
+ end
+ end
+ local base = Integer.one()
+ for _, term in ipairs(factorization:subexpressions()) do
+ base = base * term.expressions[1] ^ (term.expressions[2] / power)
+ end
+ return true, base, power
+end
+
+--- Returns the prime factorization of this integer as a expression.
+--- @return Expression
+function Integer:primefactorization()
+ if not Integer.FACTORIZATIONLIMIT then
+ Integer.FACTORIZATIONLIMIT = Integer(Integer.DIGITSIZE)
+ end
+ if self > Integer.FACTORIZATIONLIMIT then
+ return self
+ end
+ local result = self:primefactorizationrec()
+ local mul = {}
+ local i = 1
+ for factor, degree in pairs(result) do
+ mul[i] = BinaryOperation.POWEXP({factor, degree})
+ i = i + 1
+ end
+ return BinaryOperation.MULEXP(mul):lock(Expression.NIL)
+end
+
+--- Recursive part of prime factorization using Pollard Rho.
+function Integer:primefactorizationrec()
+ if self < Integer.zero() then
+ return Integer.mergefactors({[Integer(-1)]=Integer.one()}, (-self):primefactorizationrec())
+ end
+ if self == Integer.one() then
+ return {[Integer.one()]=Integer.one()}
+ end
+ local result = self:findafactor()
+ if result == self then
+ return {[result]=Integer.one()}
+ end
+ local remaining = self / result
+ return Integer.mergefactors(result:primefactorizationrec(), remaining:primefactorizationrec())
+end
+
+
+function Integer.mergefactors(a, b)
+ local result = Copy(a)
+
+ for factor, degree in pairs(b) do
+ for ofactor, odegree in pairs(result) do
+ if factor == ofactor then
+ result[ofactor] = degree + odegree
+ goto continue
+ end
+ end
+ result[factor] = degree
+ ::continue::
+ end
+ return result
+end
+
+-- Return a non-trivial factor of n via Pollard Rho, or returns n if n is prime.
+function Integer:findafactor()
+ if self:isprime() then
+ return self
+ end
+
+ if self % Integer(2) == Integer.zero() then
+ return Integer(2)
+ end
+
+ if self % Integer(3) == Integer.zero() then
+ return Integer(3)
+ end
+
+ if self % Integer(5) == Integer.zero() then
+ return Integer(5)
+ end
+
+ local g = function(x)
+ local temp = Integer.powmod(x, Integer(2), self)
+ return temp
+ end
+
+ local xstart = Integer(2)
+ while xstart < self do
+ local x = xstart
+ local y = xstart
+ local d = Integer.one()
+ while d == Integer.one() do
+ x = g(x)
+ y = g(g(y))
+ d = Integer.gcd((x - y):abs(), self)
+ end
+
+ if d < self then
+ return d
+ end
+
+ xstart = xstart + Integer.one()
+ end
+end
+
+--- Uses Miller-Rabin to determine whether a number is prime up to a very large number.
+local smallprimes = {Integer:new(2), Integer:new(3), Integer:new(5), Integer:new(7), Integer:new(11), Integer:new(13), Integer:new(17),
+Integer:new(19), Integer:new(23), Integer:new(29), Integer:new(31), Integer:new(37), Integer:new(41), Integer:new(43), Integer:new(47)}
+
+function Integer:isprime()
+ if self % Integer(2) == Integer.zero() then
+ if self == Integer(2) then
+ return true
+ end
+ return false
+ end
+
+ if self == Integer.one() then
+ return false
+ end
+
+ for _, value in pairs(smallprimes) do
+ if value == self then
+ return true
+ end
+ end
+
+ local r = Integer.zero()
+ local d = self - Integer.one()
+ while d % Integer(2) == Integer.zero() do
+ r = r + Integer.one()
+ d = d / Integer(2)
+ end
+
+ for _, a in ipairs(smallprimes) do
+ local s = r
+ local x = Integer.powmod(a, d, self)
+ if x == Integer.one() or x == self - Integer.one() then
+ goto continue
+ end
+
+ while s > Integer.zero() do
+ x = Integer.powmod(x, Integer(2), self)
+ if x == self - Integer.one() then
+ goto continue
+ end
+ s = s - Integer.one()
+ end
+ do
+ return false
+ end
+ ::continue::
+ end
+
+ return true
+end
+
+--- Returns the absolute value of an integer.
+--- @return Integer
+function Integer:abs()
+ if self.sign >= 0 then
+ return Integer(self)
+ end
+ return -self
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__Integer.__index = EuclideanDomain
+__Integer.__call = Integer.new
+Integer = setmetatable(Integer, __Integer)
+
+----------------------
+-- Static constants --
+----------------------
+
+Integer.FACTORIZATIONLIMIT = Integer(Integer.DIGITSIZE) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/integerquotientring.lua b/macros/luatex/latex/luacas/tex/algebra/integerquotientring.lua
new file mode 100644
index 0000000000..4c2188c9e5
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/integerquotientring.lua
@@ -0,0 +1,197 @@
+--- @class IntegerModN
+--- Represents an element of the ring of integers mod n (this is also a field iff n is prime).
+--- @field element Integer
+--- @field modulus Integer
+
+IntegerModN = {}
+__IntegerModN = {}
+
+-- Metatable for ring objects.
+local __obj = {__index = IntegerModN, __eq = function(a, b)
+ return a["ring"] == b["ring"] and (a["modulus"] == b["modulus"] or a["modulus"] == nil or b["modulus"] == nil)
+end, __tostring = function(a)
+ if a.modulus then return "Z/Z" .. tostring(a.modulus) else return "(Generic Integer Mod Ring)" end
+end}
+
+--------------------------
+-- Static functionality --
+--------------------------
+
+--- Creates a new ring with the given modulus.
+--- @param modulus Integer
+--- @return RingIdentifier
+function IntegerModN.makering(modulus)
+ local t = {ring = IntegerModN}
+ t.modulus = modulus
+ t = setmetatable(t, __obj)
+ return t
+end
+
+-- Shorthand constructor for a ring with a particular modulus.
+function IntegerModN.R(modulus)
+ return IntegerModN.makering(modulus)
+end
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+-- So we don't have to copy the field operations each time
+local __o
+__o = Copy(__FieldOperations)
+
+__o.__index = IntegerModN
+__o.__tostring = function(a)
+ return tostring(a.element)
+end
+
+--- Creates a new integer i in Z/nZ.
+--- @param i Integer
+--- @param n Integer
+--- @return IntegerModN
+function IntegerModN:new(i, n)
+ local o = {}
+
+ if n:getring() ~= Integer:getring() or n < Integer.one() then
+ error("Argument error: modulus must be an integer greater than 0.")
+ end
+
+ o = setmetatable(o, __o)
+
+ if i < Integer.zero() or i >= n then
+ i = i % n
+ end
+
+ o.element = i
+ o.modulus = n
+
+ return o
+end
+
+--- @return RingIdentifier
+function IntegerModN:getring()
+ local t = {ring = IntegerModN}
+ if self then
+ t.modulus = self.modulus
+ end
+ t = setmetatable(t, __obj)
+ return t
+end
+
+--- @param ring RingIdentifier
+--- @return Ring
+function IntegerModN:inring(ring)
+ if ring == IntegerModN:getring() then
+ if ring.modulus then
+ return IntegerModN(self.element, ring.modulus)
+ end
+ return self
+ end
+
+ if ring == PolynomialRing:getring() then
+ return PolynomialRing({self:inring(ring.child)}, ring.symbol)
+ end
+
+ if ring == Rational:getring() and ring.symbol then
+ return Rational(self:inring(ring.child), self:inring(ring.child):one(), true)
+ end
+
+ if ring == Integer:getring() then
+ return self.element:inring(ring)
+ end
+
+ error("Unable to convert element to proper ring.")
+end
+
+--- @param b IntegerModN
+--- @return IntegerModN
+function IntegerModN:add(b)
+ return IntegerModN(self.element + b.element, self.modulus)
+end
+
+--- @return IntegerModN
+function IntegerModN:neg()
+ return IntegerModN(-self.element, self.modulus)
+end
+
+--- @param b IntegerModN
+--- @return IntegerModN
+function IntegerModN:mul(b)
+ return IntegerModN(self.element * b.element, self.modulus)
+end
+
+-- Overrides the generic power method with powmod.
+--- @param b IntegerModN
+--- @return IntegerModN
+function IntegerModN:pow(b)
+ return IntegerModN(Integer.powmod(self.element, b.element, self.modulus), self.modulus)
+end
+
+-- Returns the multiplicative inverse of this number if it exists.
+--- @return IntegerModN
+function IntegerModN:inv()
+ local r, t, _ = Integer.extendedgcd(self.element, self.modulus)
+
+ if r > Integer.one() then
+ error("Element does not have an inverse in this ring")
+ end
+
+ return IntegerModN(t, self.modulus)
+end
+
+--- @param b IntegerModN
+--- @return IntegerModN
+function IntegerModN:div(b)
+ return self:mul(b:inv())
+end
+
+--- @param b IntegerModN
+--- @return boolean
+function IntegerModN:eq(b)
+ return self.element == b.element
+end
+
+--- @param b IntegerModN
+--- @return boolean
+function IntegerModN:lt(b)
+ return self.element < b.element
+end
+
+--- @param b IntegerModN
+--- @return boolean
+function IntegerModN:le(b)
+ return self.element <= b.element
+end
+
+--- @return IntegerModN
+function IntegerModN:zero()
+ if not self or not self.modulus then
+ return Integer.zero()
+ end
+ return IntegerModN(Integer.zero(), self.modulus)
+end
+
+--- @return IntegerModN
+function IntegerModN:one()
+ if not self or not self.modulus then
+ return Integer.one()
+ end
+ return IntegerModN(Integer.one(), self.modulus)
+end
+
+--- @return string
+function IntegerModN:tolatex(mod)
+ mod = mod or false
+ if mod then
+ return self.element:tolatex() .. "\\bmod{" .. self.modulus:tolatex() .. "}"
+ else
+ return self.element:tolatex()
+ end
+end
+-----------------
+-- Inheritance --
+-----------------
+
+__IntegerModN.__index = Field
+__IntegerModN.__call = IntegerModN.new
+IntegerModN = setmetatable(IntegerModN, __IntegerModN) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/logarithm.lua b/macros/luatex/latex/luacas/tex/algebra/logarithm.lua
new file mode 100644
index 0000000000..07a9db84d1
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/logarithm.lua
@@ -0,0 +1,186 @@
+--- @class Logarithm
+--- An expression for the logarithm of an expression with respect to another.
+--- Currently, logarithms are not being evaluated since we are just doing symbolic computation.
+--- @field base Expression
+--- @field expression Expression
+Logarithm = {}
+__Logarithm = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new logarithm expression with the given symbol and expression.
+--- @param base Expression
+--- @param expression Expression
+--- @
+function Logarithm:new(base, expression)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ o.base = Copy(base)
+ o.expression = Copy(expression)
+
+ __o.__index = Logarithm
+ __o.__tostring = function(a)
+ return 'log(' .. tostring(base) .. ', ' .. tostring(expression) .. ')'
+ end
+ __o.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway
+ if not b:type() == Logarithm then
+ return false
+ end
+ return a.base == b.base and a.expression == b.expression
+ end
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- @return Expression
+function Logarithm:evaluate()
+
+ if (self.base:isconstant() and (self.base <= Integer.zero() or self.base == Integer.one())) or
+ (self.expression:isconstant() and self.expression <= Integer.zero()) then
+ error("Arithmetic error: division by zero")
+ end
+
+ if not self.base:isconstant() or not self.expression:isconstant() then
+ return self
+ end
+
+ local power = Integer.one()
+ local base = self.base
+ if base:type() == Integer then
+ local pp, b, p = base:isperfectpower()
+ if pp then
+ base = b
+ power = p
+ end
+ end
+
+ if base:type() == Rational then
+ local ppn, bn, pn = base.numerator:isperfectpower()
+ local ppd, bd, pd = base.denominator:isperfectpower()
+ if base.numerator == Integer.one() then
+ ppn = true
+ bn = Integer.one()
+ pn = pd
+ end
+ if ppn and ppd and pn == pd then
+ base = bn / bd
+ power = pn
+ end
+ end
+
+ local result = Integer.one()
+ local expression = self.expression
+ local sign = Integer.one()
+ if base < Integer.one() then
+ base = Integer.one() / base
+ sign = -sign
+ end
+
+
+ local current = base
+ while current < expression do
+ current = current * base
+ result = result + Integer.one()
+ end
+ if current == expression then
+ return sign * result / power
+ else
+ while current > expression do
+ current = current / base
+ result = result - Integer.one()
+ end
+ if current == expression then
+ return sign * result / power
+ end
+ end
+
+ return self
+end
+
+--- @return Expression
+function Logarithm:autosimplify()
+
+ local base = self.base:autosimplify()
+ local expression = self.expression:autosimplify()
+
+ local evaluated = Logarithm(base, expression):evaluate()
+ if evaluated:type() ~= Logarithm then
+ return evaluated
+ end
+
+ -- Uses the property that log(b, 1) = 0
+ if expression == Integer.one() then
+ return Integer.zero()
+ end
+
+ -- Uses the property that log(b, b) = 1
+ if expression == base then
+ return Integer.one()
+ end
+
+ -- Uses the propery that log(b, x^y) = y * log(b, x)
+ if expression.operation == BinaryOperation.POW then
+ return BinaryOperation.MULEXP({expression.expressions[2], Logarithm(base, expression.expressions[1])}):autosimplify()
+ end
+
+ if expression:type() == Rational and expression.numerator == Integer.one() then
+ return (-Logarithm(base, expression.denominator)):autosimplify()
+ end
+
+ -- Our expression cannot be simplified
+ return Logarithm(base, expression)
+end
+
+--- @return Expression
+function Logarithm:expand()
+ return Logarithm(self.base:expand(), self.expression:expand()):autosimplify()
+end
+
+--- @return table<number, Expression>
+function Logarithm:subexpressions()
+ return {self.base, self.expression}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return Logarithm
+function Logarithm:setsubexpressions(subexpressions)
+ return Logarithm(subexpressions[1], subexpressions[2])
+end
+
+--- @param other Expression
+--- @return boolean
+function Logarithm:order(other)
+ return FunctionExpression("log", {self.base, self.expression}):order(other)
+end
+
+--- @return string
+function Logarithm:tolatex()
+ if self.base == E then
+ return '\\ln\\mathopen{}\\left(' .. self.expression:tolatex() .. '\\right)'
+ end
+ return '\\log_' .. self.base:tolatex() .. '\\mathopen{}\\left(' .. self.expression:tolatex() .. '\\right)'
+end
+
+-----------------
+-- Inheritance --
+-----------------
+__Logarithm.__index = CompoundExpression
+__Logarithm.__call = Logarithm.new
+Logarithm = setmetatable(Logarithm, __Logarithm)
+
+----------------------
+-- Static constants --
+----------------------
+
+LOG = function(base, expression)
+ return Logarithm(base, expression)
+end
+
+LN = function(expression)
+ return Logarithm(E, expression)
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/polynomialring.lua b/macros/luatex/latex/luacas/tex/algebra/polynomialring.lua
new file mode 100644
index 0000000000..568c21c921
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/polynomialring.lua
@@ -0,0 +1,860 @@
+--- @class PolynomialRing
+--- Represents an element of a polynomial ring.
+--- @field coefficients table<number, Ring>
+--- @field symbol SymbolExpression
+--- @field ring RingIdentifier
+PolynomialRing = {}
+__PolynomialRing = {}
+
+-- Metatable for ring objects.
+local __obj = {__index = PolynomialRing, __eq = function(a, b)
+ return a["ring"] == b["ring"] and
+ (a["child"] == b["child"] or a["child"] == nil or b["child"] == nil) and
+ (a["symbol"] == b["symbol"] or a["child"] == nil or b["child"] == nil)
+end, __tostring = function(a)
+ if a.child and a.symbol then return tostring(a.child) .. "[" .. a.symbol .. "]" else return "(Generic Polynomial Ring)" end
+end}
+
+--------------------------
+-- Static functionality --
+--------------------------
+
+--- Creates a new ring with the given symbol and child ring.
+--- @param symbol SymbolExpression
+--- @param child RingIdentifier
+--- @return RingIdentifier
+function PolynomialRing.makering(symbol, child)
+ local t = {ring = PolynomialRing}
+ t.symbol = symbol
+ t.child = child
+ t = setmetatable(t, __obj)
+ return t
+end
+
+-- Shorthand constructor for a polynomial ring with integer or integer mod ring coefficients.
+function PolynomialRing.R(symbol, modulus)
+ if modulus then
+ return PolynomialRing.makering(symbol, IntegerModN.makering(modulus))
+ end
+ return PolynomialRing.makering(symbol, Integer.getring())
+end
+
+--- Returns the GCD of two polynomials in a ring, assuming both rings are euclidean domains.
+--- @param a PolynomialRing
+--- @param b PolynomialRing
+--- @return PolynomialRing
+function PolynomialRing.gcd(a, b)
+ if a.symbol ~= b.symbol then
+ error("Cannot take the gcd of two polynomials with different symbols")
+ end
+ while b ~= Integer.zero() do
+ a, b = b, a % b
+ end
+ return a // a:lc()
+end
+
+-- Returns the GCD of two polynomials in a ring, assuming both rings are euclidean domains.
+-- Also returns bezouts coefficients via extended gcd.
+--- @param a PolynomialRing
+--- @param b PolynomialRing
+--- @return PolynomialRing, PolynomialRing, PolynomialRing
+function PolynomialRing.extendedgcd(a, b)
+ local oldr, r = a, b
+ local olds, s = Integer.one(), Integer.zero()
+ local oldt, t = Integer.zero(), Integer.one()
+ while r ~= Integer.zero() do
+ local q = oldr // r
+ oldr, r = r, oldr - q*r
+ olds, s = s, olds - q*s
+ oldt, t = t, oldt - q*t
+ end
+ return oldr // oldr:lc(), olds // oldr:lc(), oldt // oldr:lc()
+end
+
+-- Returns the resultant of two polynomials in the same ring, whose coefficients are all part of a field.
+--- @param a PolynomialRing
+--- @param b PolynomialRing
+--- @return Field
+function PolynomialRing.resultant(a, b)
+
+ if a.ring == PolynomialRing.getring() or b.ring == PolynomialRing.getring() then
+ return PolynomialRing.resultantmulti(a, b)
+ end
+
+ local m, n = a.degree, b.degree
+ if n == Integer.zero() then
+ return b.coefficients[0]^m
+ end
+
+ local r = a % b
+ if r == Integer.zero() then
+ return r.coefficients[0]
+ end
+
+ local s = r.degree
+ local l = b:lc()
+
+ return Integer(-1)^(m*n) * l^(m-s) * PolynomialRing.resultant(b, r)
+end
+
+-- Returns the resultant of two polynomials in the same ring, whose coefficients are not part of a field.
+--- @param a PolynomialRing
+--- @param b PolynomialRing
+--- @return Ring
+function PolynomialRing.resultantmulti(a, b)
+ local m, n = a.degree, b.degree
+
+ if m < n then
+ return Integer(-1) ^ (m * n) * PolynomialRing.resultantmulti(b, a)
+ end
+ if n == Integer.zero() then
+ return b.coefficients[0]^m
+ end
+
+ local delta = m - n + Integer(1)
+ local _ , r = PolynomialRing.pseudodivide(a, b)
+ if r == Integer.zero() then
+ return r.coefficients[0]
+ end
+
+ local s = r.degree
+ local w = Integer(-1)^(m*n) * PolynomialRing.resultant(b, r)
+ local l = b:lc()
+ local k = delta * n - m + s
+ local f = l ^ k
+ return w // f
+end
+
+-- Given two polynomials a and b, returns a list of the remainders generated by the monic Euclidean algorithm.
+--- @param a PolynomialRing
+--- @param b PolynomialRing
+--- @return table<number, Ring>
+function PolynomialRing.monicgcdremainders(a, b)
+ if a.symbol ~= b.symbol then
+ error("Cannot take the gcd of two polynomials with different symbols")
+ end
+
+ local remainders = {a / a:lc(), b / b:lc()}
+ while true do
+ local q = remainders[#remainders - 1] // remainders[#remainders]
+ local c = remainders[#remainders - 1] - q*remainders[#remainders]
+ if c ~= Integer.zero() then
+ remainders[#remainders+1] = c/c:lc()
+ else
+ break
+ end
+ end
+
+ return remainders
+end
+
+-- Returns the partial fraction decomposition of the rational function g/f
+-- given g, f, and some (not nessecarily irreducible) factorization of f.
+-- If the factorization is omitted, the irreducible factorization is used.
+-- The degree of g must be less than the degree of f.
+--- @param g PolynomialRing
+--- @param f PolynomialRing
+--- @param ffactors Expression
+--- @return Expression
+function PolynomialRing.partialfractions(g, f, ffactors)
+
+ if g.degree >= f.degree then
+ error("Argument Error: The degree of g must be less than the degree of f.")
+ end
+
+ -- Converts f to a monic polynomial.
+ g = g * f:lc()
+ f = f / f:lc()
+
+ ffactors = ffactors or f:factor()
+
+ local expansionterms = {}
+
+ for _, factor in ipairs(ffactors.expressions) do
+ local k
+ local m
+ if factor.getring and factor:getring() == PolynomialRing:getring() then
+ m = factor
+ k = Integer.one()
+ elseif not factor:isconstant() then
+ m = factor.expressions[1]
+ k = factor.expressions[2]
+ end
+
+ if not factor:isconstant() then
+ -- Uses Chinese Remainder Theorem for each factor to determine the numerator of the term in the decomposition
+ local mk = m^k
+ local v = g % mk
+ local _, minv, _ = PolynomialRing.extendedgcd(f // mk, mk)
+ local c = v*minv % mk
+
+
+ if k == Integer.one() then
+ expansionterms[#expansionterms+1] = BinaryOperation.ADDEXP({BinaryOperation.DIVEXP({c, BinaryOperation.POWEXP({m, Integer.one()})})})
+ else
+ -- Uses the p-adic expansion of c to split terms with repeated roots.
+ local q = c
+ local r
+ local innerterms = {}
+ for i = k:asnumber(), 1, -1 do
+ q, r = q:divremainder(m)
+ innerterms[#innerterms+1] = BinaryOperation.DIVEXP({r, BinaryOperation.POWEXP({m, Integer(i)})})
+ end
+ expansionterms[#expansionterms+1] = BinaryOperation.ADDEXP(innerterms)
+ end
+ end
+ end
+
+ return BinaryOperation.ADDEXP(expansionterms)
+
+end
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+-- So we don't have to copy the Euclidean operations each time
+local __o = Copy(__EuclideanOperations)
+__o.__index = PolynomialRing
+__o.__tostring = function(a)
+ local out = ""
+ local loc = a.degree:asnumber()
+ while loc >= 0 do
+ if a.ring == PolynomialRing.getring() or (a.ring == Rational.getring() and a.ring.symbol) then
+ out = out .. "(" .. tostring(a.coefficients[loc]) .. ")" .. a.symbol .. "^" .. tostring(math.floor(loc)) .. "+"
+ else
+ out = out .. tostring(a.coefficients[loc]) .. a.symbol .. "^" .. tostring(math.floor(loc)) .. "+"
+ end
+ loc = loc - 1
+ end
+ return string.sub(out, 1, string.len(out) - 1)
+end
+__o.__div = function(a, b)
+ if not b.getring then
+ return BinaryOperation.DIVEXP({a, b})
+ end
+ if Ring.resultantring(a.ring, b:getring()) ~= Ring.resultantring(a:getring(), b:getring()) then
+ return a:div(b:inring(Ring.resultantring(a:getring(), b:getring())))
+ end
+ if b.ring and b:getring() == Rational:getring() and a.symbol == b.ring.symbol then
+ return a:inring(Ring.resultantring(a:getring(), b:getring())):div(b)
+ end
+ if a:getring() == b:getring() then
+ return Rational(a, b, true)
+ end
+ -- TODO: Fix this for arbitrary depth
+ if a:getring() == PolynomialRing:getring() and b:getring() == PolynomialRing:getring() and a.symbol == b.symbol then
+ local oring = Ring.resultantring(a:getring(), b:getring())
+ return Rational(a:inring(oring), b:inring(oring), true)
+ end
+ return BinaryOperation.DIVEXP({a, b})
+end
+
+function PolynomialRing:tolatex()
+ local out = ''
+ local loc = self.degree:asnumber()
+ if loc == 0 then
+ return self.coefficients[loc]:tolatex()
+ end
+ if self.ring == Rational.getring() or self.ring == Integer.getring() or self.ring == IntegerModN.getring() then
+ if self.coefficients[loc] ~= Integer.one() then
+ out = out .. self.coefficients[loc]:tolatex() .. self.symbol
+ else
+ out = out .. self.symbol
+ end
+ if loc ~=1 then
+ out = out .. "^{" .. loc .. "}"
+ end
+ loc = loc -1
+ while loc >=0 do
+ local coeff = self.coefficients[loc]
+ if coeff == Integer.one() then
+ if loc == 0 then
+ out = out .. "+" .. coeff:tolatex()
+ goto skip
+ else
+ out = out .. "+"
+ goto continue
+ end
+ end
+ if coeff == Integer(-1) then
+ if loc == 0 then
+ out = out .. "-" .. coeff:neg():tolatex()
+ goto skip
+ else
+ out = out .. "-"
+ goto continue
+ end
+ end
+ if coeff < Integer.zero() then
+ out = out .. "-" .. coeff:neg():tolatex()
+ end
+ if coeff == Integer.zero() then
+ goto skip
+ end
+ if coeff > Integer.zero() then
+ out = out .. "+" .. coeff:tolatex()
+ end
+ ::continue::
+ if loc > 1 then
+ out = out .. self.symbol .. "^{" .. loc .. "}"
+ end
+ if loc == 1 then
+ out = out .. self.symbol
+ end
+ ::skip::
+ loc = loc-1
+ end
+ else
+ while loc >=0 do
+ if loc >=1 then
+ out = out .. self.coefficients[loc]:tolatex() .. self.symbol .. "^{" .. loc .. "} + "
+ else
+ out = out .. self.coefficients[loc]:tolatex() .. self.symbol .. "^{" .. loc .. "}"
+ end
+ loc = loc-1
+ end
+ end
+ return out
+end
+
+function PolynomialRing:isatomic()
+ --if self.degree >= Integer.one() then
+ -- return false
+ --else
+ return false
+ --end
+end
+--test
+
+-- Creates a new polynomial ring given an array of coefficients and a symbol
+function PolynomialRing:new(coefficients, symbol, degree)
+ local o = {}
+ o = setmetatable(o, __o)
+
+ if type(coefficients) ~= "table" then
+ error("Sent parameter of wrong type: Coefficients must be in an array")
+ end
+ o.coefficients = {}
+ o.degree = degree or Integer(-1)
+
+ if type(symbol) ~= "string" and not symbol.symbol then
+ error("Symbol must be a string")
+ end
+ o.symbol = symbol.symbol or symbol
+
+ -- Determines what ring the polynomial ring should have as its child
+ for index, coefficient in pairs(coefficients) do
+ if type(index) ~= "number" then
+ error("Sent parameter of wrong type: Coefficients must be in an array")
+ end
+ if not coefficient.getring then
+ error("Sent parameter of wrong type: Coefficients must be elements of a ring")
+ end
+ if not o.ring then
+ o.ring = coefficient:getring()
+ else
+ local newring = coefficient:getring()
+ local combinedring = Ring.resultantring(o.ring, newring)
+ if combinedring == newring then
+ o.ring = newring
+ elseif not o.ring == combinedring then
+ error("Sent parameter of wrong type: Coefficients must all be part of the same ring")
+ end
+ end
+ end
+
+ if not coefficients[0] then
+ -- Constructs the coefficients when a new polynomial is instantiated as an array
+ for index, coefficient in ipairs(coefficients) do
+ o.coefficients[index - 1] = coefficient
+ o.degree = o.degree + Integer.one()
+ end
+ else
+ -- Constructs the coefficients from an existing polynomial of coefficients
+ local loc = o.degree:asnumber()
+ while loc > 0 do
+ if not coefficients[loc] or coefficients[loc] == coefficients[loc]:zero() then
+ o.degree = o.degree - Integer.one()
+ else
+ break
+ end
+ loc = loc - 1
+ end
+
+ while loc >= 0 do
+ o.coefficients[loc] = coefficients[loc]
+ loc = loc - 1
+ end
+ end
+
+ -- Each value of the polynomial greater than its degree is implicitly zero
+ o.coefficients = setmetatable(o.coefficients, {__index = function (table, key)
+ return o:zeroc()
+ end})
+ return o
+end
+
+-- Returns the ring this object is an element of
+function PolynomialRing:getring()
+ local t = {ring = PolynomialRing}
+ if self then
+ t.child = self.ring
+ t.symbol = self.symbol
+ end
+ t = setmetatable(t, __obj)
+ return t
+end
+
+-- Explicitly converts this element to an element of another ring
+function PolynomialRing:inring(ring)
+
+ -- Faster equality check
+ if ring == self:getring() then
+ return self
+ end
+
+ if ring == Rational:getring() and ring.symbol then
+ return Rational(self:inring(ring.child), self:inring(ring.child):one(), true)
+ end
+
+ if ring.symbol == self.symbol then
+ local out = {}
+ for i = 0, self.degree:asnumber() do
+ out[i + 1] = self.coefficients[i]:inring(ring.child)
+ end
+ return PolynomialRing(out, self.symbol)
+ end
+
+ -- TODO: Allow re-ordering of polynomial rings, so from R[x][y] -> R[y][x] for instance
+ if ring == PolynomialRing:getring() then
+ return PolynomialRing({self:inring(ring.child)}, ring.symbol)
+ end
+
+ error("Unable to convert element to proper ring.")
+end
+
+
+-- Returns whether the ring is commutative
+function PolynomialRing:iscommutative()
+ return true
+end
+
+function PolynomialRing:add(b)
+ local larger
+
+ if self.degree > b.degree then
+ larger = self
+ else
+ larger = b
+ end
+
+ local new = {}
+ local loc = 0
+ while loc <= larger.degree:asnumber() do
+ new[loc] = self.coefficients[loc] + b.coefficients[loc]
+ loc = loc + 1
+ end
+
+ return PolynomialRing(new, self.symbol, larger.degree)
+end
+
+function PolynomialRing:neg()
+ local new = {}
+ local loc = 0
+ while loc <= self.degree:asnumber() do
+ new[loc] = -self.coefficients[loc]
+ loc = loc + 1
+ end
+ return PolynomialRing(new, self.symbol, self.degree)
+end
+
+function PolynomialRing:mul(b)
+ -- Grade-school multiplication is actually faster up to a very large polynomial size due to Lua's overhead.
+ local new = {}
+
+ local sd = self.degree:asnumber()
+ local bd = b.degree:asnumber()
+
+ for i = 0, sd+bd do
+ new[i] = self:zeroc()
+ for j = math.max(0, i-bd), math.min(sd, i) do
+ new[i] = new[i] + self.coefficients[j]*b.coefficients[i-j]
+ end
+ end
+ return PolynomialRing(new, self.symbol, self.degree + b.degree)
+ -- return PolynomialRing(PolynomialRing.mul_rec(self.coefficients, b.coefficients), self.symbol, self.degree + b.degree)
+end
+
+-- Performs Karatsuba multiplication without constructing new polynomials recursively
+function PolynomialRing.mul_rec(a, b)
+ if #a==0 and #b==0 then
+ return {[0]=a[0] * b[0], [1]=Integer.zero()}
+ end
+
+ local k = Integer.ceillog(Integer.max(Integer(#a), Integer(#b)) + Integer.one(), Integer(2))
+ local n = Integer(2) ^ k
+ local m = n / Integer(2)
+ local nn = n:asnumber()
+ local mn = m:asnumber()
+
+ local a0, a1, b0, b1 = {}, {}, {}, {}
+
+ for e = 0, mn - 1 do
+ a0[e] = a[e] or Integer.zero()
+ a1[e] = a[e + mn] or Integer.zero()
+ b0[e] = b[e] or Integer.zero()
+ b1[e] = b[e + mn] or Integer.zero()
+ end
+
+ local p1 = PolynomialRing.mul_rec(a1, b1)
+ local p2a = Copy(a0)
+ local p2b = Copy(b0)
+ for e = 0, mn - 1 do
+ p2a[e] = p2a[e] + a1[e]
+ p2b[e] = p2b[e] + b1[e]
+ end
+ local p2 = PolynomialRing.mul_rec(p2a, p2b)
+ local p3 = PolynomialRing.mul_rec(a0, b0)
+ local r = {}
+ for e = 0, mn - 1 do
+ p2[e] = p2[e] - p1[e] - p3[e]
+ r[e] = p3[e]
+ r[e + mn] = p2[e]
+ r[e + nn] = p1[e]
+ end
+ for e = mn, nn - 1 do
+ p2[e] = p2[e] - p1[e] - p3[e]
+ r[e] = r[e] + p3[e]
+ r[e + mn] = r[e + mn] + p2[e]
+ r[e + nn] = p1[e]
+ end
+
+ return r
+end
+
+-- Uses synthetic division.
+function PolynomialRing:divremainder(b)
+ local n, m = self.degree:asnumber(), b.degree:asnumber()
+
+ if m > n then
+ return self:zero(), self
+ end
+
+ local o = Copy(self.coefficients)
+ local lc = b:lc()
+ for i = n, m, -1 do
+ o[i] = o[i] / lc
+
+ if o[i] ~= self:zeroc() then
+ for j = 1, m do
+ o[i-j] = o[i-j] - b.coefficients[m - j] * o[i]
+ end
+ end
+ end
+
+ local q = {}
+ local r = {}
+ for i = 0, m-1 do
+ r[i] = o[i]
+ end
+
+ r[0] = r[0] or self:zeroc()
+
+ for i = m, #o do
+ q[i - m] = o[i]
+ end
+
+ return PolynomialRing(q, self.symbol, self.degree), PolynomialRing(r, self.symbol, Integer.max(Integer.zero(), b.degree-Integer.one()))
+end
+
+-- Performs polynomial pseudodivision of this polynomial by another in the same ring,
+-- and returns both the pseudoquotient and pseudoremainder.
+-- In the case where both coefficients are fields, this is equivalent to division with remainder.
+function PolynomialRing:pseudodivide(b)
+
+ local p = self:zero()
+ local s = self
+ local m = s.degree
+ local n = b.degree
+ local delta = Integer.max(m - n + Integer.one(), Integer.zero())
+
+ local lcb = b:lc()
+ local sigma = Integer.zero()
+
+ while m >= n and s ~= Integer.zero() do
+ local lcs = s:lc()
+ p = p * lcb + self:one():multiplyDegree((m-n):asnumber()) * lcs
+ s = s * lcb - b * self:one():multiplyDegree((m-n):asnumber()) * lcs
+ sigma = sigma + Integer.one()
+ m = s.degree
+ end
+
+ if delta - sigma == Integer.zero() then
+ return p,s
+ else
+ return lcb^(delta - sigma) * p, lcb^(delta - sigma) * s
+ end
+end
+
+-- Polynomial rings are never fields, but when dividing by a polynomial by a constant we may want to use / instead of //
+function PolynomialRing:div(b)
+ return self:divremainder(b)
+end
+
+function PolynomialRing:zero()
+ return self.coefficients[0]:zero():inring(self:getring())
+end
+
+function PolynomialRing:zeroc()
+ return self.coefficients[0]:zero()
+end
+
+function PolynomialRing:one()
+ return self.coefficients[0]:one():inring(self:getring())
+end
+
+function PolynomialRing:onec()
+ return self.coefficients[0]:one()
+end
+
+function PolynomialRing:eq(b)
+ for i=0,math.max(self.degree:asnumber(), b.degree:asnumber()) do
+ if self.coefficients[i] ~= b.coefficients[i] then
+ return false
+ end
+ end
+ return true
+end
+
+-- Returns the leading coefficient of this polynomial
+function PolynomialRing:lc()
+ return self.coefficients[self.degree:asnumber()]
+end
+
+--- @return boolean
+function PolynomialRing:isconstant()
+ return false
+end
+
+-- This expression is free of a symbol if and only if the symbol is not the symbol used to create the ring.
+function PolynomialRing:freeof(symbol)
+ return symbol.symbol ~= self.symbol
+end
+
+-- Replaces each expression in the map with its value.
+function PolynomialRing:substitute(map)
+ return self:tocompoundexpression():substitute(map)
+end
+
+-- Expands a polynomial expression. Polynomials are already in expanded form, so we just need to autosimplify.
+function PolynomialRing:expand()
+ return self:tocompoundexpression():autosimplify()
+end
+
+function PolynomialRing:autosimplify()
+ return self:tocompoundexpression():autosimplify()
+end
+
+-- Transforms from array format to an expression format.
+function PolynomialRing:tocompoundexpression()
+ local terms = {}
+ for exponent, coefficient in pairs(self.coefficients) do
+ terms[exponent + 1] = BinaryOperation(BinaryOperation.MUL, {coefficient:tocompoundexpression(),
+ BinaryOperation(BinaryOperation.POW, {SymbolExpression(self.symbol), Integer(exponent)})})
+ end
+ return BinaryOperation(BinaryOperation.ADD, terms)
+end
+
+-- Uses Horner's rule to evaluate a polynomial at a point
+function PolynomialRing:evaluateat(x)
+ local out = self:zeroc()
+ for i = self.degree:asnumber(), 1, -1 do
+ out = out + self.coefficients[i]
+ out = out * x
+ end
+ return out + self.coefficients[0]
+end
+
+-- Multiplies this polynomial by x^n
+function PolynomialRing:multiplyDegree(n)
+ local new = {}
+ for e = 0, n-1 do
+ new[e] = self:zeroc()
+ end
+ local loc = n
+ while loc <= self.degree:asnumber() + n do
+ new[loc] = self.coefficients[loc - n]
+ loc = loc + 1
+ end
+ return PolynomialRing(new, self.symbol, self.degree + Integer(n))
+end
+
+-- Returns the formal derivative of this polynomial
+function PolynomialRing:derivative()
+ if self.degree == Integer.zero() then
+ return PolynomialRing({self:zeroc()}, self.symbol, Integer(-1))
+ end
+ local new = {}
+ for e = 1, self.degree:asnumber() do
+ new[e - 1] = Integer(e) * self.coefficients[e]
+ end
+ return PolynomialRing(new, self.symbol, self.degree - Integer.one())
+end
+
+-- Returns the square-free factorization of a polynomial
+function PolynomialRing:squarefreefactorization()
+ local terms
+ if self.ring == Rational.getring() or self.ring == Integer.getring() then
+ terms = self:rationalsquarefreefactorization()
+ elseif self.ring == IntegerModN.getring() then
+ if not self.ring.modulus:isprime() then
+ error("Cannot compute a square-free factorization of a polynomial ring contructed from a ring that is not a field.")
+ end
+ terms = self:modularsquarefreefactorization()
+ end
+
+ local expressions = {self:lc()}
+ local j = 1
+ for index, term in ipairs(terms) do
+ if term.degree ~= Integer.zero() or term.coefficients[0] ~= Integer.one() then
+ j = j + 1
+ expressions[j] = BinaryOperation.POWEXP({term, Integer(index)})
+ end
+ end
+
+ return BinaryOperation.MULEXP(expressions)
+end
+
+-- Factors a polynomial into irreducible terms
+function PolynomialRing:factor()
+ -- Square-free factorization over an integral domain (so a polynomial ring constructed from a field)
+ local squarefree = self:squarefreefactorization()
+ local squarefreeterms = {}
+ local result = {squarefree.expressions[1]}
+ for i, expression in ipairs(squarefree.expressions) do
+ if i > 1 then
+ -- Converts square-free polynomials with rational coefficients to integer coefficients so Rational Roots / Zassenhaus can factor them
+ if expression.expressions[1].ring == Rational.getring() then
+ local factor, integerpoly = expression.expressions[1]:rationaltointeger()
+ result[1] = result[1] * factor ^ expression.expressions[2]
+ squarefreeterms[i - 1] = integerpoly
+ else
+ squarefreeterms[i - 1] = expression.expressions[1]
+ end
+ end
+ end
+
+ for i, expression in ipairs(squarefreeterms) do
+ local terms
+ if expression.ring == Integer.getring() then
+ -- Factoring over the integers first uses the rational roots test to factor out monomials (for efficiency purposes)
+ local remaining, factors = expression:rationalroots()
+ terms = factors
+ -- Then applies the Zassenhaus algorithm if there entire polynomial has not been factored into monomials
+ if remaining ~= Integer.one() then
+ remaining = remaining:zassenhausfactor()
+ for _, exp in ipairs(remaining) do
+ terms[#terms+1] = exp
+ end
+ end
+ end
+ if expression.ring == IntegerModN.getring() then
+ -- Berlekamp factorization is used for rings with integers mod a prime as coefficients
+ terms = expression:berlekampfactor()
+ end
+ for _, factor in ipairs(terms) do
+ result[#result+1] = BinaryOperation.POWEXP({factor, squarefree.expressions[i + 1].expressions[2]})
+ end
+ end
+ return BinaryOperation.MULEXP(result)
+end
+
+-- Uses the Rational Root test to factor out monomials of a square-free polynomial.
+function PolynomialRing:rationalroots()
+ local remaining = self
+ local roots = {}
+ if self.coefficients[0] == Integer.zero() then
+ roots[1] = PolynomialRing({Integer.zero(), Integer.one()}, self.symbol)
+ remaining = remaining // roots[1]
+ end
+ -- This can be slower than Zassenhaus if the digits are large enough, since factoring integers is slow
+ -- if self.coefficients[0] > Integer(Integer.DIGITSIZE - 1) or self:lc() > Integer(Integer.DIGITSIZE - 1) then
+ -- return remaining, roots
+ -- end
+ while remaining ~= Integer.one() do
+ :: nextfactor ::
+ local a = remaining.coefficients[0]
+ local b = remaining:lc()
+ local afactors = a:divisors()
+ local bfactors = b:divisors()
+ for _, af in ipairs(afactors) do
+ for _, bf in ipairs(bfactors) do
+ local testroot = Rational(af, bf, true)
+ if remaining:evaluateat(testroot) == Integer.zero() then
+ roots[#roots+1] = PolynomialRing({-testroot.numerator, testroot.denominator}, self.symbol)
+ remaining = remaining // roots[#roots]
+ goto nextfactor
+ end
+ if remaining:evaluateat(-testroot) == Integer.zero() then
+ roots[#roots+1] = PolynomialRing({testroot.numerator, testroot.denominator}, self.symbol)
+ remaining = remaining // roots[#roots]
+ goto nextfactor
+ end
+ end
+ end
+ break
+ end
+
+ return remaining, roots
+end
+
+-- Returns a list of roots of the polynomial, simplified up to cubics.
+function PolynomialRing:roots()
+ local roots = {}
+ local factorization = self:factor()
+
+ for i, factor in ipairs(factorization.expressions) do
+ if i > 1 then
+ local decomp = factor.expressions[1]:decompose()
+ for _, poly in ipairs(decomp) do
+ if poly.degree > Integer(3) then
+ table.insert(roots,RootExpression(factor.expressions[1]))
+ goto nextfactor
+ end
+ end
+ local factorroots = RootExpression(decomp[#decomp]):autosimplify()
+ if factorroots == true then
+ return true
+ end
+ if factorroots == false then
+ goto nextfactor
+ end
+ local replaceroots = {}
+ for j = #decomp - 1,1,-1 do
+ for _, root in ipairs(factorroots) do
+ local temp = RootExpression(decomp[j]):autosimplify(root)
+ if temp == true then
+ return true
+ end
+ if factorroots == false then
+ goto nextfactor
+ end
+ replaceroots = JoinArrays(replaceroots, temp)
+ end
+ factorroots = replaceroots
+ end
+ roots = JoinArrays(roots, factorroots)
+ end
+ end
+ ::nextfactor::
+ return roots
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__PolynomialRing.__index = Ring
+__PolynomialRing.__call = PolynomialRing.new
+PolynomialRing = setmetatable(PolynomialRing, __PolynomialRing) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/polynomialring/berlekampfactoring.lua b/macros/luatex/latex/luacas/tex/algebra/polynomialring/berlekampfactoring.lua
new file mode 100644
index 0000000000..feabb61a3f
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/polynomialring/berlekampfactoring.lua
@@ -0,0 +1,187 @@
+-- Methods related to the Berlekamp factoring algorithm.
+
+-- Square-free factorization in the modular field Zp.
+function PolynomialRing:modularsquarefreefactorization()
+ local monic = self / self:lc()
+ local terms = {}
+ terms[0] = PolynomialRing.gcd(monic, monic:derivative())
+ local b = monic // terms[0]
+ local c = monic:derivative() // terms[0]
+ local d = c - b:derivative()
+ local i = 1
+ while b ~= Integer.one() do
+ terms[i] = PolynomialRing.gcd(b, d)
+ b, c = b // terms[i], d // terms[i]
+ i = i + 1
+ d = c - b:derivative()
+ end
+
+ if not (terms[i-1]:derivative().degree == Integer.zero() and terms[i-1]:derivative().coefficients[0] == Integer.zero()) then
+ return terms
+ end
+
+ local recursiveterms = terms[i-1]:collapseterms(self.ring.modulus):modularsquarefreefactorization()
+ for k, poly in ipairs(recursiveterms) do
+ recursiveterms[k] = poly:expandterms(self.ring.modulus)
+ end
+ return JoinArrays(terms, recursiveterms)
+end
+
+-- Returns a new polnomial consisting of every nth term of the old one - helper method for square-free factorization
+function PolynomialRing:collapseterms(n)
+ local new = {}
+ local loc = 0
+ local i = 0
+ local nn = n:asnumber()
+ while loc <= self.degree:asnumber() do
+ new[i] = self.coefficients[loc]
+ loc = loc + nn
+ i = i + 1
+ end
+
+ return PolynomialRing(new, self.symbol, self.degree // n)
+end
+
+-- Returns a new polnomial consisting of every nth term of the old one - helper method for square-free factorization
+function PolynomialRing:expandterms(n)
+ local new = {}
+ local loc = 0
+ local i = 0
+ local nn = n:asnumber()
+ while i <= self.degree:asnumber() do
+ new[loc] = self.coefficients[i]
+ for j = 1, nn do
+ new[loc + j] = IntegerModN(Integer.zero(), n)
+ end
+ loc = loc + nn
+ i = i + 1
+ end
+
+ return PolynomialRing(new, self.symbol, self.degree * n)
+end
+
+-- Uses Berlekamp's Algorithm to factor polynomials in mod p
+function PolynomialRing:berlekampfactor()
+ if self.degree == 0 or self.degree == 1 then
+ return {self}
+ end
+
+ local R = self:RMatrix()
+ local S = self:auxillarybasis(R)
+ if #S == 1 then
+ return {self}
+ end
+ return self:findfactors(S)
+end
+
+-- Gets the R Matrix for Berlekamp factorization
+function PolynomialRing:RMatrix()
+ local R = {}
+ for i = 1, self.degree:asnumber() do
+ R[i] = {}
+ end
+ for i = 0, self.degree:asnumber()-1 do
+ local remainder = PolynomialRing({IntegerModN(Integer.one(), self.ring.modulus)}, self.symbol):multiplyDegree(self.ring.modulus:asnumber()*i) % self
+ for j = 0, self.degree:asnumber()-1 do
+ R[j + 1][i + 1] = remainder.coefficients[j]
+ if j == i then
+ R[j + 1][i + 1] = R[j + 1][i + 1] - IntegerModN(Integer.one(), self.ring.modulus)
+ end
+ end
+ end
+ return R
+end
+
+-- Creates an auxillary basis using the R matrix
+function PolynomialRing:auxillarybasis(R)
+ local P = {}
+ local n = self.degree:asnumber()
+ for i = 1, n do
+ P[i] = 0
+ end
+ S = {}
+ local q = 1
+ for j = 1, n do
+ local i = 1
+ local pivotfound = false
+ while not pivotfound and i <= n do
+ if R[i][j] ~= self:zeroc() and P[i] == 0 then
+ pivotfound = true
+ else
+ i = i + 1
+ end
+ end
+ if pivotfound then
+ P[i] = j
+ local a = R[i][j]:inv()
+ for l = 1, n do
+ R[i][l] = a * R[i][l]
+ end
+ for k = 1, n do
+ if k ~= i then
+ local f = R[k][j]
+ for l = 1, n do
+ R[k][l] = R[k][l] - f*R[i][l]
+ end
+ end
+ end
+ else
+ local s = {}
+ s[j] = self:onec()
+ for l = 1, j - 1 do
+ local e = 0
+ i = 1
+ while e == 0 and i <= n do
+ if l == P[i] then
+ e = i
+ else
+ i = i + 1
+ end
+ end
+ if e > 0 then
+ local c = -R[e][j]
+ s[l] = c
+ else
+ s[l] = self:zeroc()
+ end
+ end
+ S[#S+1] = PolynomialRing(s, self.symbol)
+ end
+ end
+ return S
+end
+
+-- Uses the auxilary basis to find the irreirrducible factors of the polynomial.
+function PolynomialRing:findfactors(S)
+ local r = #S
+ local p = self.ring.modulus
+ local factors = {self}
+ for k = 2,r do
+ local b = S[k]
+ local old_factors = Copy(factors)
+ for i = 1,#old_factors do
+ local w = old_factors[i]
+ local j = 0
+ while j <= p:asnumber() - 1 do
+ local g = PolynomialRing.gcd(b-IntegerModN(Integer(j), p), w)
+ if g == Integer.one() then
+ j = j + 1
+ elseif g == w then
+ j = p:asnumber()
+ else
+ factors = Remove(factors, w)
+ local q = w // g
+ factors[#factors+1] = g
+ factors[#factors+1] = q
+ if #factors == r then
+ return factors
+ else
+ j = j + 1
+ w = q
+ end
+ end
+
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/polynomialring/decomposition.lua b/macros/luatex/latex/luacas/tex/algebra/polynomialring/decomposition.lua
new file mode 100644
index 0000000000..84f2af1efc
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/polynomialring/decomposition.lua
@@ -0,0 +1,93 @@
+-- Methods related to polynomial decomposition.
+
+-- Returns a list of polynomials that form a complete decomposition of a polynomial.
+function PolynomialRing:decompose()
+ local U = self - self.coefficients[0]
+ local S = U:divisors()
+ local decomposition = {}
+ local C = PolynomialRing({Integer.zero(), Integer.one()}, self.symbol)
+ local finalcomponent
+
+ while S[1] do
+ local w = S[1]
+ for _, poly in ipairs(S) do
+ if poly.degree < w.degree then
+ w = poly
+ end
+ end
+ S = Remove(S, w)
+ if C.degree < w.degree and w.degree < self.degree and self.degree % w.degree == Integer.zero() then
+ local g = w:polyexpand(C, self.symbol)
+ local R = self:polyexpand(w, self.symbol)
+ if g.degree == Integer.zero() and R.degree == Integer.zero() then
+ g.symbol = self.symbol
+ decomposition[#decomposition+1] = g.coefficients[0]
+ decomposition[#decomposition].symbol = self.symbol
+ C = w
+ finalcomponent = R.coefficients[0]
+ end
+ end
+ end
+
+ if not decomposition[1] then
+ return {self}
+ end
+
+ finalcomponent.symbol = self.symbol
+ decomposition[#decomposition+1] = finalcomponent
+ return decomposition
+end
+
+-- Returns a list of all monic divisors of positive degree of the polynomial, assuming the polynomial ring is a Euclidean Domain.
+function PolynomialRing:divisors()
+ local factors = self:factor()
+ -- Converts each factor to a monic factor (we don't need to worry updating the constant term)
+ for i, factor in ipairs(factors.expressions) do
+ if i > 1 then
+ factor.expressions[1] = factor.expressions[1] / factor.expressions[1]:lc()
+ end
+ end
+
+ local terms = {}
+ for i, _ in ipairs(factors.expressions) do
+ if i > 1 then
+ terms[i] = Integer.zero()
+ end
+ end
+
+ local divisors = {}
+ local divisor = PolynomialRing({self:onec()}, self.symbol)
+ while true do
+ for i, factor in ipairs(factors.expressions) do
+ if i > 1 then
+ local base = factor.expressions[1]
+ local power = factor.expressions[2]
+ if terms[i] < power then
+ terms[i] = terms[i] + Integer.one()
+ divisor = divisor * base
+ break
+ else
+ terms[i] = Integer.zero()
+ divisor = divisor // (base ^ power)
+ end
+ end
+ end
+ if divisor == Integer.one() then
+ break
+ end
+ divisors[#divisors+1] = divisor
+ end
+
+ return divisors
+
+end
+
+-- Polynomial expansion as a subroutine of decomposition.
+function PolynomialRing:polyexpand(v, x)
+ local u = self
+ if u == Integer.zero() then
+ return Integer.zero()
+ end
+ local q,r = u:divremainder(v)
+ return PolynomialRing({PolynomialRing({Integer.zero(), Integer.one()}, "_")}, x) * q:polyexpand(v, x) + r
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/polynomialring/zassenhausfactoring.lua b/macros/luatex/latex/luacas/tex/algebra/polynomialring/zassenhausfactoring.lua
new file mode 100644
index 0000000000..be19298e64
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/polynomialring/zassenhausfactoring.lua
@@ -0,0 +1,220 @@
+-- Methods related to the Zassenhaus factorization algorithm.
+
+
+-- Square-free factorization in the rational field.
+function PolynomialRing:rationalsquarefreefactorization(keeplc)
+ local monic = self / self:lc()
+ local terms = {}
+ terms[0] = PolynomialRing.gcd(monic, monic:derivative())
+ local b = monic // terms[0]
+ local c = monic:derivative() // terms[0]
+ local d = c - b:derivative()
+ local i = 1
+ while b.degree ~= Integer.zero() or b.coefficients[0] ~= Integer.one() do
+ terms[i] = PolynomialRing.gcd(b, d)
+ b, c = b // terms[i], d // terms[i]
+ i = i + 1
+ d = c - b:derivative()
+ end
+ if keeplc and terms[1] then
+ terms[1] = terms[1] * self:lc()
+ end
+ return terms
+end
+
+-- Factors the largest possible constant out of a polynomial whos underlying ring is a Euclidean domain but not a field
+function PolynomialRing:factorconstant()
+ local gcd = Integer.zero()
+ for i = 0, self.degree:asnumber() do
+ gcd = self.ring.gcd(gcd, self.coefficients[i])
+ end
+ if gcd == Integer.zero() then
+ return Integer.one(), self
+ end
+ return gcd, self / gcd
+end
+
+-- Converts a polynomial in the rational polynomial ring to the integer polynomial ring
+function PolynomialRing:rationaltointeger()
+ local lcm = Integer.one()
+ for i = 0, self.degree:asnumber() do
+ if self.coefficients[i]:getring() == Rational:getring() then
+ lcm = lcm * self.coefficients[i].denominator / Integer.gcd(lcm, self.coefficients[i].denominator)
+ end
+ end
+ return Integer.one() / lcm, self * lcm
+end
+
+-- Uses Zassenhaus's Algorithm to factor sqaure-free polynomials over the intergers
+function PolynomialRing:zassenhausfactor()
+
+ -- Creates a monic polynomial V with related roots
+ local V = {}
+ local n = self.degree:asnumber()
+ local l = self:lc()
+ for i = 0, n - 1 do
+ V[i] = l ^ Integer(n - 1 - i) * self.coefficients[i]
+ end
+ V[n] = Integer.one()
+ V = PolynomialRing(V, "y", self.degree)
+
+ -- Performs Berlekamp Factorization in a sutable prime base
+ local p = V:findprime()
+ local S = V:inring(PolynomialRing.R("y", p)):berlekampfactor()
+
+ -- If a polynomial is irreducible with coefficients in mod p, it is also irreducible over the integers
+ if #S == 1 then
+ return {self}
+ end
+
+ -- Performs Hensel lifting on the factors mod p
+ local k = V:findmaxlifts(p)
+ local W = V:henselift(S, k)
+ local M = {}
+
+ -- Returns the solutions back to the original from the monic transformation
+ for i, factor in ipairs(W) do
+ local w = {}
+ for j = 0, factor.degree:asnumber() do
+ w[j] = factor.coefficients[j]:inring(Integer.getring()) * l ^ Integer(j)
+ end
+ _, M[i] = PolynomialRing(w, self.symbol, factor.degree):factorconstant()
+ end
+
+ return M
+
+end
+
+-- Finds the smallest prime such that this polynomial with coefficients in mod p is square-free
+function PolynomialRing:findprime()
+
+ local smallprimes = {Integer(2), Integer(3), Integer(5), Integer(7), Integer(11), Integer(13), Integer(17), Integer(19), Integer(23),
+ Integer(29), Integer(31), Integer(37), Integer(41), Integer(43), Integer(47), Integer(53), Integer(59)}
+
+ for _, p in pairs(smallprimes) do
+ local P = PolynomialRing({IntegerModN(Integer.one(), p)}, self.symbol)
+ local s = self:inring(P:getring())
+ if PolynomialRing.gcd(s, s:derivative()) == P then
+ return p
+ end
+ end
+
+ error("Execution error: No suitable prime found for factoring.")
+end
+
+-- Finds the maximum number of times Hensel Lifting will be applied to raise solutions to the appropriate power
+function PolynomialRing:findmaxlifts(p)
+ local n = self.degree:asnumber()
+ local h = self.coefficients[0]
+ for i=0 , n do
+ if self.coefficients[i] > h then
+ h = self.coefficients[i]
+ end
+ end
+
+ local B = 2^n * math.sqrt(n) * h:asnumber()
+ return Integer(math.ceil(math.log(2*B, p:asnumber())))
+end
+
+-- Uses Hensel lifting on the factors of a polynomial S mod p to find them in the integers
+function PolynomialRing:henselift(S, k)
+ local p = S[1].ring.modulus
+ if k == Integer.one() then
+ return self:truefactors(S, p, k)
+ end
+ G = self:genextendsigma(S)
+ local V = S
+ for j = 2, k:asnumber() do
+ local Vp = V[1]:inring(PolynomialRing.R("y"))
+ for i = 2, #V do
+ Vp = Vp * V[i]:inring(PolynomialRing.R("y"))
+ end
+ local E = self - Vp:inring(PolynomialRing.R("y"))
+ if E == Integer.zero() then
+ return V
+ end
+ E = E:inring(PolynomialRing.R("y", p ^ Integer(j))):inring(PolynomialRing.R("y"))
+ F = E / p ^ (Integer(j) - Integer.one())
+ R = self:genextendR(V, G, F)
+ local Vnew = {}
+ for i, v in ipairs(V) do
+ local vnew = v:inring(PolynomialRing.R("y", p ^ Integer(j)))
+ local rnew = R[i]:inring(PolynomialRing.R("y", p ^ Integer(j)))
+ Vnew[i] = vnew + (p) ^ (Integer(j) - Integer.one()) * rnew
+ end
+ V = Vnew
+ end
+ return self:truefactors(V, p, k)
+end
+
+-- Gets a list of sigma polynomials for use in hensel lifting
+function PolynomialRing:genextendsigma(S)
+ local v = S[1] * S[2]
+ local _, A, B = PolynomialRing.extendedgcd(S[2], S[1])
+ local SIGMA = {A, B}
+ for i, _ in ipairs(S) do
+ if i >= 3 then
+ v = v * S[i]
+ local sum = SIGMA[1] * (v // S[1])
+ for j = 2, i-1 do
+ sum = sum + SIGMA[j] * (v // S[j])
+ end
+ _, A, B = PolynomialRing.extendedgcd(sum, v // S[i])
+ for j = 1, i-1 do
+ SIGMA[j] = SIGMA[j] * A
+ end
+ SIGMA[i] = B
+ end
+ end
+
+ return SIGMA
+end
+
+-- Gets a list of r polynomials for use in hensel lifting
+function PolynomialRing:genextendR(V, G, F)
+ R = {}
+ for i, v in ipairs(V) do
+ local pring = G[1]:getring()
+ R[i] = F:inring(pring) * G[i] % v:inring(pring)
+ end
+ return R
+end
+
+-- Updates factors of the polynomial to the correct ones in the integer ring
+function PolynomialRing:truefactors(l, p, k)
+ local U = self
+ local L = l
+ local factors = {}
+ local m = 1
+ while m <= #L / 2 do
+ local C = Subarrays(L, m)
+ while #C > 0 do
+ local t = C[1]
+ local prod = t[1]
+ for i = 2, #t do
+ prod = prod * t[i]
+ end
+ local T = prod:inring(PolynomialRing.R("y", p ^ k)):inring(PolynomialRing.R("y"))
+ -- Convert to symmetric representation - this is the only place it actually matters
+ for i = 0, T.degree:asnumber() do
+ if T.coefficients[i] > p ^ k / Integer(2) then
+ T.coefficients[i] = T.coefficients[i] - p^k
+ end
+ end
+ local Q, R = U:divremainder(T)
+ if R == Integer.zero() then
+ factors[#factors+1] = T
+ U = Q
+ L = RemoveAll(L, t)
+ C = RemoveAny(C, t)
+ else
+ C = Remove(C, t)
+ end
+ end
+ m = m + 1
+ end
+ if U ~= Integer.one() then
+ factors[#factors+1] = U
+ end
+ return factors
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/rational.lua b/macros/luatex/latex/luacas/tex/algebra/rational.lua
new file mode 100644
index 0000000000..811909a948
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/rational.lua
@@ -0,0 +1,241 @@
+--- @class Rational
+--- Represents an element of the field of rational numbers or rational functions.
+--- @field numerator Ring
+--- @field denominator Ring
+--- @field ring RingIdentifier
+Rational = {}
+__Rational = {}
+
+
+--------------------------
+-- Static functionality --
+--------------------------
+
+-- Metatable for ring objects.
+local __obj = {__index = Rational, __eq = function(a, b)
+ return a["ring"] == b["ring"] and
+ (a["child"] == b["child"] or a["child"] == nil or b["child"] == nil) and
+ (a["symbol"] == b["symbol"] or a["child"] == nil or b["child"] == nil)
+end, __tostring = function(a)
+ if a.symbol then
+ return tostring(a.child.child) .. "(" .. a.symbol .. ")"
+ end
+ if a.child then
+ return "QQ"
+ end
+ return "(Generic Fraction Field)"
+ end}
+
+--- @param symbol SymbolExpression
+--- @param child RingIdentifier
+--- @return RingIdentifier
+function Rational.makering(symbol, child)
+ local t = {ring = Rational}
+ t.symbol = symbol
+ t.child = child
+ t = setmetatable(t, __obj)
+ return t
+end
+
+--- Converts a string of the form -?[0-9]+ or -?[0-9]+\/[0-9]+ to a rational number.
+--- @param str string
+--- @return Rational|Integer
+function Rational.fromstring(str)
+ local divloc = string.find(str, "/");
+ if not divloc then
+ return Integer(str)
+ end
+ return Rational(Integer(string.sub(str, 1, divloc - 1)), Integer(string.sub(str, divloc + 1, #str)))
+end
+
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+-- So we don't have to copy the field operations each time.
+local __o = Copy(__FieldOperations)
+__o.__index = Rational
+__o.__tostring = function(a)
+ if a.ring.symbol then
+ return "(" .. tostring(a.numerator)..")/("..tostring(a.denominator) .. ")"
+ end
+ return tostring(a.numerator).."/"..tostring(a.denominator)
+end
+
+--- Creates a new rational given a numerator and denominator that are part of the same ring.
+--- Rational numbers are represented uniquely.
+--- @param n Ring
+--- @param d Ring
+--- @param keep boolean
+function Rational:new(n, d, keep)
+ local o = {}
+ o = setmetatable(o, __o)
+
+ if n:getring() == PolynomialRing.getring() then
+ o.symbol = n.symbol
+ end
+
+ if d:getring() == PolynomialRing.getring() then
+ o.symbol = d.symbol
+ end
+
+ if d == Integer(0) then
+ error("Arithmetic error: division by zero")
+ end
+
+ n = n or Integer.zero()
+ d = d or Integer.one()
+ o.numerator = n
+ o.denominator = d
+ o:reduce()
+
+ if (not keep) and o.denominator == Integer.one() or (not keep) and o.numerator == Integer.zero() then
+ return o.numerator
+ end
+
+ return o
+end
+
+--- Reduces a rational expression to standard form. This method mutates its object.
+function Rational:reduce()
+ if self.numerator:getring() == Integer.getring() then
+ if self.denominator < Integer.zero() then
+ self.denominator = -self.denominator
+ self.numerator = -self.numerator
+ end
+ local gcd = Integer.gcd(self.numerator, self.denominator)
+ self.numerator = self.numerator//gcd
+ self.denominator = self.denominator//gcd
+ self.ring = Integer.getring()
+ elseif self.numerator:getring() == PolynomialRing.getring() then
+ local lc = self.denominator:lc()
+ self.denominator = self.denominator/lc
+ self.numerator = self.numerator/lc
+ local gcd = PolynomialRing.gcd(self.numerator, self.denominator)
+ self.numerator = self.numerator//gcd
+ self.denominator = self.denominator//gcd
+ self.ring = Ring.resultantring(self.numerator:getring(), self.denominator:getring())
+ end
+end
+
+
+--- @return RingIdentifier
+function Rational:getring()
+ local t = {ring=Rational}
+ if self then
+ t.child = self.ring
+ t.symbol = self.symbol
+ end
+ t = setmetatable(t, __obj)
+ return t
+end
+
+--- @param ring RingIdentifier
+--- @return Ring
+function Rational:inring(ring)
+ if ring == self:getring() then
+ return self
+ end
+
+ if ring == Rational:getring() and ring.symbol then
+ if not self:getring().symbol then
+ return Rational(self:inring(ring.child), self:inring(ring.child):one(), true)
+ end
+ return Rational(self.numerator:inring(ring.child), self.denominator:inring(ring.child), true)
+ end
+
+ if ring == PolynomialRing:getring() then
+ return PolynomialRing({self:inring(ring.child)}, ring.symbol)
+ end
+
+ error("Unable to convert element to proper ring.")
+end
+
+--- @return boolean
+function Rational:isconstant()
+ if self.symbol then
+ return false
+ end
+ return true
+end
+
+--- @return Expression
+function Rational:tocompoundexpression()
+ return BinaryOperation(BinaryOperation.DIV, {self.numerator:tocompoundexpression(), self.denominator:tocompoundexpression()})
+end
+
+--- Returns this rational as a floating point number. Can only approximate the value of most rationals.
+--- @return number
+function Rational:asnumber()
+ return self.numerator:asnumber() / self.denominator:asnumber()
+end
+
+function Rational:add(b)
+ return Rational(self.numerator * b.denominator + self.denominator * b.numerator, self.denominator * b.denominator)
+end
+
+function Rational:neg()
+ return Rational(-self.numerator, self.denominator, true)
+end
+
+function Rational:mul(b)
+ return Rational(self.numerator * b.numerator, self.denominator * b.denominator)
+end
+
+-- function Rational:inv(b)
+-- return Rational(self.numerator * b.numerator, self.denominator * b.denominator)
+-- end
+
+function Rational:pow(b)
+ return (self.numerator ^ b) / (self.denominator ^ b)
+end
+
+function Rational:div(b)
+ return Rational(self.numerator * b.denominator, self.denominator * b.numerator)
+end
+
+function Rational:eq(b)
+ return self.numerator == b.numerator and self.denominator == b.denominator
+end
+
+function Rational:lt(b)
+ if self.numerator < Integer.zero() and b.numerator > Integer.zero() then
+ return true
+ end
+ if self.numerator > Integer.zero() and b.numerator < Integer.zero() then
+ return false
+ end
+
+ if (self.numerator >= Integer.zero() and b.numerator >= Integer.zero()) or (self.numerator <= Integer.zero() and b.numerator <= Integer.zero()) then
+ return self.numerator * b.denominator < self.denominator * b.numerator
+ end
+ return self.numerator * b.denominator > self.denominator * b.numerator
+end
+
+function Rational:le(b)
+ return self:eq(b) or self:lt(b)
+end
+
+function Rational:zero()
+ return Integer.zero()
+end
+
+function Rational:one()
+ return Integer.one()
+end
+
+function Rational:tolatex()
+ if string.sub(self.numerator:tolatex(),1,1) == '-' then
+ return "- \\frac{" .. string.sub(self.numerator:tolatex(),2,-1) .. "}{" .. self.denominator:tolatex() .. "}"
+ end
+ return "\\frac{" .. self.numerator:tolatex() .."}{".. self.denominator:tolatex().. "}"
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__Rational.__index = Field
+__Rational.__call = Rational.new
+Rational = setmetatable(Rational, __Rational) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/ring.lua b/macros/luatex/latex/luacas/tex/algebra/ring.lua
new file mode 100644
index 0000000000..4903b9c912
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/ring.lua
@@ -0,0 +1,326 @@
+--- @class Ring
+--- Interface for an element of a ring with unity.
+Ring = {}
+__Ring = {}
+
+--------------------------
+-- Static functionality --
+--------------------------
+
+--- Determines which ring the output of a binary operation with inputs in ring1 and ring2 should be, if such a ring exists.
+--- If one of the rings is a subring of another ring, the result should be one of the two rings.
+--- @param ring1 RingIdentifier
+--- @param ring2 RingIdentifier
+--- @return RingIdentifier
+function Ring.resultantring(ring1, ring2)
+ if ring1 == ring2 then
+ return ring1
+ end
+
+ if ((ring1 == PolynomialRing.getring() and ring2 == Rational.getring()) or
+ (ring2 == PolynomialRing.getring() and ring1 == Rational.getring()))
+ and ring1.symbol == ring2.symbol then
+ return Rational.makering(ring1.symbol, Ring.resultantring(ring1.child, ring2.child))
+ end
+
+ if ring1 == PolynomialRing.getring() or ring2 == PolynomialRing.getring() then
+ if ring1 == ring2.child then
+ return ring2
+ end
+ if ring2 == ring1.child then
+ return ring1
+ end
+
+ if ring1 == PolynomialRing.getring() and ring2 == PolynomialRing.getring() and ring1.symbol == ring2.symbol then
+ return PolynomialRing.makering(ring1.symbol, Ring.resultantring(ring1.child, ring2.child))
+ end
+
+ -- If none of the above conditions are satisfied, recusion is a pain, so we just strip all of the variables off of both rings.
+ -- TODO: Make this properly recursive, or just use a multivariable polynomial ring class
+ local symbols = {}
+ while ring1 == PolynomialRing.getring() do
+ symbols[#symbols+1] = ring1.symbol
+ ring1 = ring1.child
+ end
+ while ring2 == PolynomialRing.getring() do
+ if not Contains(symbols, ring2.symbol) then
+ symbols[#symbols+1] = ring2.symbol
+ end
+ ring2 = ring2.child
+ end
+ local ring = Ring.resultantring(ring1, ring2)
+
+ if ring == Rational.getring() and Contains(symbols, ring.symbol) then
+ symbols = Remove(symbols, ring.symbol)
+ end
+ for i = #symbols, 1, -1 do
+ ring = PolynomialRing.makering(symbols[i], ring)
+ end
+ return ring
+ end
+
+ if ring1 == Integer.getring() then
+ if ring2 == Integer.getring() then
+ return ring2
+ end
+
+ if ring2 == Rational.getring() then
+ return ring2
+ end
+
+ if ring2 == IntegerModN.getring() then
+ return ring2
+ end
+ end
+
+ if ring1 == Rational.getring() then
+ if ring2 == Integer.getring() then
+ return ring1
+ end
+
+ if ring2 == Rational.getring() then
+ if not ring1.symbol then
+ return Rational.makering(ring2.symbol, Ring.resultantring(ring1, ring2.child))
+ end
+ if not ring2.symbol then
+ return Rational.makering(ring1.symbol, Ring.resultantring(ring1.child, ring2))
+ end
+ if ring1.symbol and ring2.symbol and ring1.symbol == ring2.symbol then
+ return Rational.makering(ring1.symbol, Ring.resultantring(ring1.child, ring2.child))
+ end
+ return ring2
+ end
+
+ if ring2 == IntegerModN.getring() then
+ return nil
+ end
+ end
+
+ if ring1 == IntegerModN.getring() then
+ if ring2 == Integer.getring() then
+ return ring1
+ end
+
+ if ring2 == Rational.getring() then
+ return nil
+ end
+
+ if ring2 == IntegerModN.getring() then
+ return IntegerModN.makering(Integer.gcd(ring1.modulus, ring2.modulus))
+ end
+ end
+
+ return nil
+end
+
+--- Returns a particular instantiation of a ring.
+--- Does the same thing as getring() if there is only one possible ring for a class, i.e., the integers and rationals.
+--- @return RingIdentifier
+function Ring.makering()
+ error("Called unimplemented method : makering()")
+end
+
+----------------------
+-- Required methods --
+----------------------
+
+--- Returns the ring this element is part of.
+--- @return RingIdentifier
+function Ring:getring()
+ error("Called unimplemented method : getring()")
+end
+
+--- Explicitly converts this element to an element of another ring.
+--- @param ring RingIdentifier
+--- @return Ring
+function Ring:inring(ring)
+ error("Called unimplemented method : in()")
+end
+
+--- Returns whether the ring is commutative.
+--- @return boolean
+function Ring:iscommutative()
+ error("Called unimplemented method : iscommutative()")
+end
+
+--- @return Ring
+function Ring:add(b)
+ error("Called unimplemented method : add()")
+end
+
+--- @return Ring
+function Ring:sub(b)
+ return(self:add(b:neg()))
+end
+
+--- @return Ring
+function Ring:neg()
+ error("Called unimplemented method : neg()")
+end
+
+--- @return Ring
+function Ring:mul(b)
+ error("Called unimplemented method : mul()")
+end
+
+--- Ring exponentiation by definition. Specific rings may implement more efficient methods.
+--- @return Ring
+function Ring:pow(n)
+ if(n < Integer.zero()) then
+ error("Execution error: Negative exponentiation is undefined over general rings")
+ end
+ local k = Integer.zero()
+ local b = self:one()
+ while k < n do
+ b = b * self
+ k = k + Integer.one()
+ end
+ return b
+end
+
+--- @return boolean
+function Ring:eq(b)
+ error("Execution error: Ring does not have a total order")
+end
+
+--- @return boolean
+function Ring:lt(b)
+ error("Execution error: Ring does not have a total order")
+end
+
+--- @return boolean
+function Ring:le(b)
+ error("Execution error: Ring does not have a total order")
+end
+
+--- The additive identitity of the ring.
+--- @return Ring
+function Ring:zero()
+ error("Called unimplemented method : zero()")
+end
+
+--- The multiplicative identitity of the ring.
+--- @return Ring
+function Ring:one()
+ error("Called unimplemented method : one()")
+end
+
+--------------------------
+-- Instance metamethods --
+--------------------------
+__RingOperations = {}
+
+-- Each of these methods just handles coverting each element in the ring to an instance of the proper ring, if possible,
+-- then passing the arguments to the function in a specific ring.
+
+__RingOperations.__unm = function(a)
+ return a:neg()
+end
+
+__RingOperations.__add = function(a, b)
+ if not b.getring then
+ return BinaryOperation.ADDEXP({a, b})
+ end
+
+ local aring, bring = a:getring(), b:getring()
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to add two elements of incompatable rings")
+ end
+ return a:inring(oring):add(b:inring(oring))
+end
+
+__RingOperations.__sub = function(a, b)
+ if not b.getring then
+ return BinaryOperation.SUBEXP({a, b})
+ end
+
+ local aring, bring = a:getring(), b:getring()
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to subtract two elements of incompatable rings")
+ end
+ return a:inring(oring):sub(b:inring(oring))
+end
+
+-- Allows for multiplication by writing two expressions next to each other.
+__RingOperations.__call = function (a, b)
+ return a * b
+end
+
+__RingOperations.__mul = function(a, b)
+ if not b.getring then
+ return BinaryOperation.MULEXP({a, b})
+ end
+
+ local aring, bring = a:getring(), b:getring()
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to muliply two elements of incompatable rings")
+ end
+ return a:inring(oring):mul(b:inring(oring))
+end
+
+__RingOperations.__pow = function(a, n)
+ if (not n.getring) or (n.getring and n:getring().ring ~= Integer) then
+ return BinaryOperation.POWEXP({a, n})
+ end
+
+ -- if a == a:zero() and n == Integer.zero() then
+ -- error("Cannot raise 0 to the power of 0")
+ -- end
+
+ return a:pow(n)
+end
+
+-- Comparison operations assume, of course, that the ring operation is equipped with a total order
+-- All elements of all rings need these metamethods, since in Lua comparisons on tables only fire if both objects have the table
+__RingOperations.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway
+ if not a.getring or not b.getring then
+ return false
+ end
+ local aring, bring = a:getring(), b:getring()
+ if aring == bring then
+ return a:eq(b)
+ end
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to compare two elements of incompatable rings")
+ end
+ return a:inring(oring):eq(b:inring(oring))
+end
+
+__RingOperations.__lt = function(a, b)
+ local aring, bring = a:getring(), b:getring()
+ if aring == bring then
+ return a:lt(b)
+ end
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to compare two elements of incompatable rings")
+ end
+ return a:inring(oring):lt(b:inring(oring))
+end
+
+__RingOperations.__le = function(a, b)
+ local aring, bring = a:getring(), b:getring()
+ if aring == bring then
+ return a:le(b)
+ end
+ local oring = Ring.resultantring(aring, bring)
+ if not oring then
+ error("Attempted to compare two elements of incompatable rings")
+ end
+ return a:inring(oring):le(b:inring(oring))
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__Ring.__index = ConstantExpression
+Ring = setmetatable(Ring, __Ring)
+
+--- Used for comparing and converting between rings.
+--- @class RingIdentifier
diff --git a/macros/luatex/latex/luacas/tex/algebra/rootexpression.lua b/macros/luatex/latex/luacas/tex/algebra/rootexpression.lua
new file mode 100644
index 0000000000..a66182579a
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/rootexpression.lua
@@ -0,0 +1,135 @@
+--- @class RootExpression
+--- An expression that represents the solutions to expression = 0.
+--- @field expression Expression
+RootExpression = {}
+__RootExpression = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new root expression with the given expression.
+--- @param expression Expression
+--- @return RootExpression
+function RootExpression:new(expression)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ o.expression = Copy(expression)
+
+ __o.__index = RootExpression
+ __o.__tostring = function(a)
+ return 'Root Of: (' .. tostring(a.expression) .. ')'
+ end
+ __o.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always rungs this anyway
+ if not b:type() == RootExpression then
+ return false
+ end
+ return a.expression == b.expression
+ end
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- @return Expression
+function RootExpression:autosimplify(subpart)
+ local simplified = self.expression:autosimplify()
+ local simplified, ispoly = simplified:topolynomial()
+
+ if simplified:isconstant() then
+ -- 0 = 0 is always true (obviously).
+ return simplified == simplified:zero()
+ end
+
+ if ispoly then
+ if simplified.degree == Integer.zero() then
+ return simplified == simplified:zero()
+ end
+ if simplified.degree == Integer.one() then
+ return {-simplified.coefficients[0] / simplified.coefficients[1]}
+ end
+ if simplified.degree == Integer(2) then
+ local a = simplified.coefficients[2]
+ local b = simplified.coefficients[1]
+ local c = simplified.coefficients[0]
+ -- This is a hack until we can get more expression manipulation working, but that's okay.
+ if subpart then
+ c = (c - subpart):autosimplify()
+ end
+ return {((-b + sqrt(b^Integer(2) - Integer(4) * a * c)) / (Integer(2) * a)):autosimplify(),
+ ((-b - sqrt(b^Integer(2) - Integer(4) * a * c)) / (Integer(2) * a)):autosimplify()}
+ end
+ if simplified.degree == Integer(3) then
+ local a = simplified.coefficients[3]
+ local b = simplified.coefficients[2]
+ local c = simplified.coefficients[1]
+ local d = simplified.coefficients[0]
+ -- This is a hack until we can get more expression manipulation working, but that's okay.
+ if subpart then
+ d = (d - subpart):autosimplify()
+ end
+
+ local delta0 = (b^Integer(2) - Integer(3)*a*c):autosimplify()
+ local delta1 = (Integer(2) * b^Integer(3) - Integer(9)*a*b*c+Integer(27)*a^Integer(2)*d):autosimplify()
+
+ local C = sqrt((delta1 + sqrt(delta1 ^ Integer(2) - Integer(4) * delta0 ^ Integer(3))) / Integer(2), Integer(3)):autosimplify()
+
+ if C == Integer.zero() then
+ C = sqrt((delta1 - sqrt(delta1 ^ Integer(2) - Integer(4) * delta0 ^ Integer(3))) / Integer(2), Integer(3)):autosimplify()
+ end
+
+ if C == Integer.zero() then
+ C = (-b/(Integer(3)*a)):autosimplify()
+ end
+
+ local eta = ((Integer(-1) + sqrt(Integer(-3))) / Integer(2)):autosimplify()
+
+ return {((-Integer.one() / (Integer(3) * a)) * (b + C + delta0 / C)):autosimplify(),
+ ((-Integer.one() / (Integer(3) * a)) * (b + C*eta + delta0 / (C*eta))):autosimplify(),
+ ((-Integer.one() / (Integer(3) * a)) * (b + C*eta^Integer(2) + delta0 / (C*eta^Integer(2)))):autosimplify()}
+ end
+ end
+ if ispoly then
+ simplified = simplified:autosimplify()
+ end
+ if subpart then
+ simplified = (simplified - subpart):autosimplify()
+ end
+ return {RootExpression(simplified)}
+end
+
+--- @return table<number, Expression>
+function RootExpression:subexpressions()
+ return {self.expression}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return RootExpression
+function RootExpression:setsubexpressions(subexpressions)
+ return RootExpression(subexpressions[1])
+end
+
+--- @param other Expression
+--- @return boolean
+function RootExpression:order(other)
+ --- TODO: Fix ordering on new expression types
+ if other:type() ~= RootExpression then
+ return false
+ end
+
+ return self.expression:order(other.expression)
+end
+
+--- @return string
+function RootExpression:tolatex()
+ return '\\operatorname{RootOf}\\left(' .. self.expression:tolatex() .. '\\right)'
+end
+
+-----------------
+-- Inheritance --
+-----------------
+__RootExpression.__index = CompoundExpression
+__RootExpression.__call = RootExpression.new
+RootExpression = setmetatable(RootExpression, __RootExpression) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/sqrtexpression.lua b/macros/luatex/latex/luacas/tex/algebra/sqrtexpression.lua
new file mode 100644
index 0000000000..bc96f4e1d8
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/sqrtexpression.lua
@@ -0,0 +1,196 @@
+--- @class SqrtExpression
+--- An expression that represents the positive real solution to x^n = a where n is a positive integer and a is constant.
+--- @field expression Expression
+SqrtExpression = {}
+__SqrtExpression = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new sqrt expression with the given expression.
+--- @param expression Expression
+--- @param root Integer
+--- @return SqrtExpression
+function SqrtExpression:new(expression, root)
+ root = root or Integer(2)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ o.expression = Copy(expression)
+ o.root = root
+
+ __o.__index = SqrtExpression
+ __o.__tostring = function(a)
+ return tostring(a.expression) .. ' ^ (1/' .. tostring(a.root) .. ')'
+ end
+ __o.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always rungs this anyway
+ if not b:type() == SqrtExpression then
+ return false
+ end
+ return a.expression == b.expression and a.root == b.root
+ end
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+
+--- @return table<number, Expression>
+function SqrtExpression:subexpressions()
+ return {self.expression}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return SqrtExpression
+function SqrtExpression:setsubexpressions(subexpressions)
+ return SqrtExpression(subexpressions[1], self.root)
+end
+
+--- @param other Expression
+--- @return boolean
+function SqrtExpression:order(other)
+ return self:topower():order(other)
+end
+
+function SqrtExpression:topower()
+ local exponent = BinaryOperation(BinaryOperation.DIV,{Integer.one(),self.root}):autosimplify()
+ local base = self.expression
+ return BinaryOperation(BinaryOperation.POW,{base,exponent}):autosimplify()
+end
+
+function SqrtExpression:autosimplify()
+ local expression = self.expression:autosimplify()
+ local root = self.root:autosimplify()
+
+ if root == Integer.one() then
+ return expression
+ end
+
+ if root:type() == Rational then
+ return SqrtExpression(BinaryOperation(BinaryOperation.POW,{expression,root.denominator}):autosimplify(), root.numerator):autosimplify()
+ end
+
+ if not root:isconstant() then
+ return BinaryOperation(BinaryOperation.POW,{expression,Integer.one() / root}):autosimplify()
+ end
+
+ if not expression:isconstant() then
+ if expression.operation == BinaryOperation.MUL and expression.expressions[1]:isconstant() then
+ local coeff = SqrtExpression(expression.expressions[1],root):autosimplify()
+ expression.expressions[1] = BinaryOperation(BinaryOperation.MUL,{Integer.one()})
+ expression = expression:autosimplify()
+ local sqrtpart = SqrtExpression(expression,root):autosimplify()
+ local result = coeff*sqrtpart
+ return result:autosimplify()
+ end
+ return BinaryOperation(BinaryOperation.POW,{expression,Integer.one() / root}):autosimplify()
+ end
+
+ if expression:type() == Rational then
+ local result = BinaryOperation(BinaryOperation.MUL, {SqrtExpression(expression.numerator,root):autosimplify(),BinaryOperation(BinaryOperation.POW,{SqrtExpression(expression.denominator,root):autosimplify(),Integer(-1)})})
+ return result:autosimplify()
+ end
+
+ if expression:type() == Integer then
+ if expression == Integer.zero() then
+ return Integer.zero()
+ end
+ if expression == Integer.one() then
+ return Integer.one()
+ end
+ if expression < Integer.zero() then
+ if root == Integer(2) then
+ local result = SqrtExpression(expression:neg(),root):autosimplify()
+ result = I*result
+ return result:autosimplify()
+ end
+ if root % Integer(2) == Integer.one() then
+ local result = SqrtExpression(expression:neg(),root):autosimplify()
+ result = -result
+ return result:autosimplify()
+ end
+ end
+ local primes = expression:primefactorization()
+ local coeffresult = {}
+ local exprresult = {}
+ local reduction = root
+ for _, term in ipairs(primes.expressions) do
+ local primepower = term.expressions[2]
+ reduction = Integer.gcd(primepower,reduction)
+ if reduction == Integer.one() then
+ goto skip
+ end
+ end
+ ::skip::
+ local newroot = root / reduction
+ for index, term in ipairs(primes.expressions) do
+ local prime = term.expressions[1]
+ local primepower = term.expressions[2] / reduction
+ local coeffpower = primepower // newroot
+ coeffresult[index] = prime ^ coeffpower
+ local exprpower = primepower - coeffpower*newroot
+ exprresult[index] = prime ^ exprpower
+ end
+ local newexpression = BinaryOperation(BinaryOperation.MUL,exprresult):autosimplify()
+ local coeff = BinaryOperation(BinaryOperation.MUL,coeffresult):autosimplify()
+ if coeff == Integer.one() then
+ if reduction == Integer.one() then
+ goto stop
+ end
+ return SqrtExpression(newexpression,newroot)
+ end
+ if newroot == Integer.one() then
+ return coeff
+ end
+ return BinaryOperation(BinaryOperation.MUL,{coeff,SqrtExpression(newexpression,newroot)}):autosimplify()
+ end
+ ::stop::
+
+ if expression.operation == BinaryOperation.POW and expression.expressions[2]:type() == Integer then
+ local exponent = expression.expressions[2]
+ local power = exponent // root
+ local newexponent = (exponent / root) - power
+ local coeff = expression.expressions[1] ^ power
+ coeff = coeff:evaluate()
+ if newexponent == Integer.zero() then
+ return coeff
+ else
+ local num = newexponent.numerator
+ local den = newexponent.denominator
+ local newexpression = expression ^ num
+ newexpression = newexpression:autosimplify()
+ local result = coeff * SqrtExpression(newexpression,den)
+ return result
+ end
+ end
+
+ return SqrtExpression(expression,root)
+end
+
+function SqrtExpression:tolatex()
+ local printout = '\\sqrt'
+ if self.root == Integer(2) then
+ printout = printout .. '{' .. self.expression:tolatex() .. '}'
+ else
+ printout = printout .. '[' .. self.root:tolatex() .. ']' .. '{' .. self.expression:tolatex() .. '}'
+ end
+ return printout
+end
+
+
+-----------------
+-- Inheritance --
+-----------------
+__SqrtExpression.__index = CompoundExpression
+__SqrtExpression.__call = SqrtExpression.new
+SqrtExpression = setmetatable(SqrtExpression, __SqrtExpression)
+
+----------------------
+-- Static constants --
+----------------------
+
+sqrt = function(expression, root)
+ return SqrtExpression(expression, root)
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/algebra/trigexpression.lua b/macros/luatex/latex/luacas/tex/algebra/trigexpression.lua
new file mode 100644
index 0000000000..307bfe1737
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/algebra/trigexpression.lua
@@ -0,0 +1,355 @@
+--- @class TrigExpression
+--- Represents a trigonometric function from one expression to another.
+--- @field name string
+--- @field expression Expression
+TrigExpression = {}
+__TrigExpression = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new trig expression with the given name and expression.
+--- @param name string|SymbolExpression
+--- @param expression Expression
+--- @return TrigExpression
+function TrigExpression:new(name, expression)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ if not TrigExpression.NAMES[name] then
+ error("Argument error: " .. name .. " is not the name of a trigonometric function.")
+ end
+
+ o.name = name
+ o.expression = expression
+ o.expressions = {expression}
+ if expression:isatomic() then
+ o.variables = {expression}
+ else
+ o.variables = {SymbolExpression('x')}
+ end
+ o.derivatives = {Integer.zero()}
+
+ __o.__index = TrigExpression
+ __o.__tostring = function(a)
+ return tostring(a.name) .. '(' .. tostring(a.expression) .. ')'
+ end
+ __o.__eq = function(a, b)
+ -- if b:type() == FunctionExpression then
+ -- return a:tofunction() == b
+ -- end
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway
+ if not b:type() == TrigExpression then
+ return false
+ end
+ return a.name == b.name and a.expression == b.expression
+ end
+
+ o = setmetatable(o, __o)
+ return o
+end
+
+--- @return TrigExpression
+function TrigExpression:evaluate()
+ local expression = self.expression:autosimplify()
+
+ if expression == Integer.zero() then
+ if self.name == "cos" or self.name == "sec" then
+ return Integer.one()
+ end
+ if self.name == "sin" or self.name == "tan" then
+ return Integer.zero()
+ end
+ if self.name == "arctan" or self.name == "arcsin" then
+ return Integer.zero()
+ end
+ if self.name == "arccos" or self.name == "arccot" then
+ return PI / Integer(2)
+ end
+ end
+
+ if expression == PI then
+ if self.name == "cos" or self.name == "sec" then
+ return Integer(-1)
+ end
+ if self.name == "sin" or self.name == "tan" then
+ return Integer.zero()
+ end
+ end
+
+ if expression:ismulratlPI() then
+ local coeff = expression.expressions[1]
+ if TrigExpression.COSVALUES[tostring(coeff)] ~= nil then
+ if self.name == "cos" then
+ return TrigExpression.COSVALUES[tostring(coeff)]:autosimplify()
+ end
+ if self.name == "sin" then
+ local sign = Integer.one()
+ if coeff > Integer.one() then
+ sign = Integer(-1)
+ end
+ return (sign*sqrt(Integer.one()-cos(expression)^Integer(2))):autosimplify()
+ end
+ if self.name == "tan" then
+ return (sin(expression) / cos(expression)):autosimplify()
+ end
+ if self.name == "sec" then
+ return (Integer.one() / cos(expression)):autosimplify()
+ end
+ if self.name == "csc" then
+ return (Integer.one() / sin(expression)):autosimplify()
+ end
+ if self.name == "cot" then
+ return (cos(expression) / sin(expression)):autosimplify()
+ end
+ end
+ end
+
+ if TrigExpression.ACOSVALUES[tostring(expression)] ~= nil then
+ if self.name == "arccos" then
+ return TrigExpression.ACOSVALUES[tostring(expression)]:autosimplify()
+ end
+ if self.name == "arcsin" then
+ if expression == Integer(-1) then
+ return TrigExpression.ACOSVALUES["-1"]:autosimplify()
+ elseif expression.expressions and expression.expressions[1] == Integer(-1) then
+ local expr = (Integer(-1)*sqrt(Integer.one() - expression ^ Integer(2))):autosimplify()
+ return TrigExpression.ACOSVALUES[tostring(expr)]:autosimplify()
+ else
+ local expr = (sqrt(Integer.one() - expression ^ Integer(2))):autosimplify()
+ return TrigExpression.ACOSVALUES[tostring(expr)]:autosimplify()
+ end
+ end
+ end
+
+ if self.name == "arctan" and TrigExpression.ATANVALUES[tostring(expression)] ~= nil then
+ return TrigExpression.ATANVALUES[tostring(expression)]:autosimplify()
+ end
+
+ return self
+end
+
+--- checks if expression is a rational multiple of pi
+--- @return boolean
+function Expression:ismulratlPI()
+ if self.operation == BinaryOperation.MUL and #self.expressions == 2 and (self.expressions[1]:type() == Integer or self.expressions[1]:type() == Rational) and self.expressions[2] == PI then
+ return true
+ end
+
+ return false
+end
+
+--- @return TrigExpression
+function TrigExpression:autosimplify()
+ local expression = self.expression:autosimplify()
+
+ -- even and odd properties of trig functions
+ if (self.name == "sin" or self.name == "tan" or self.name == "csc" or self.name == "cot") and
+ expression.operation == BinaryOperation.MUL and expression.expressions[1]:isconstant() and expression.expressions[1] < Integer(0) then
+ return (-Integer.one() * TrigExpression(self.name, -expression)):autosimplify()
+ end
+
+ if (self.name == "cos" or self.name == "sec") and
+ expression.operation == BinaryOperation.MUL and expression.expressions[1]:isconstant() and expression.expressions[1] < Integer(0) then
+ expression = (-expression):autosimplify()
+ end
+
+ -- uses periodicity of sin and cos and friends
+ if self.name == "sin" or self.name == "cos" or self.name == "csc" or self.name == "sec" then
+ if expression == Integer.zero() or expression == PI then
+ goto skip
+ end
+ if expression.operation ~= BinaryOperation.ADD then
+ expression = BinaryOperation(BinaryOperation.ADD,{expression})
+ end
+ for index,component in ipairs(expression.expressions) do
+ if component:ismulratlPI() then
+ local coeff = component.expressions[1]
+ if coeff:type() == Integer then
+ coeff = coeff % Integer(2)
+ coeff = coeff:autosimplify()
+ end
+ if coeff:type() == Rational then
+ local n = coeff.numerator
+ local d = coeff.denominator
+ local m = {n:divremainder(d)}
+ coeff = (m[1] % Integer(2)) + m[2]/d
+ coeff = coeff:autosimplify()
+ end
+ expression.expressions[index].expressions[1] = coeff
+ end
+ expression = expression:autosimplify()
+ end
+ ::skip::
+ end
+
+ -- uses periodicity of tan and cot
+ if self.name == "tan" or self.name == "cot" then
+ if expression == Integer.zero() or expression == PI then
+ goto skip
+ end
+ if expression.operation ~= BinaryOperation.ADD then
+ expression = BinaryOperation(BinaryOperation.ADD,{expression})
+ end
+ for index,component in ipairs(expression.expressions) do
+ if component:ismulratlPI() then
+ local coeff = component.expressions[1]
+ if coeff:type() == Integer then
+ coeff = Integer.zero()
+ end
+ if coeff:type() == Rational then
+ local n = coeff.numerator
+ local d = coeff.denominator
+ local m = {n:divremainder(d)}
+ coeff = m[2]/d
+ coeff = coeff:autosimplify()
+ end
+ expression.expressions[index].expressions[1] = coeff
+ end
+ if component == PI then
+ expression.expressions[index] = Integer.zero()
+ end
+ end
+ expression = expression:autosimplify()
+ ::skip::
+ end
+
+ return TrigExpression(self.name, expression):evaluate()
+end
+
+--- @return table<number, Expression>
+function TrigExpression:subexpressions()
+ return {self.expression}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return TrigExpression
+function TrigExpression:setsubexpressions(subexpressions)
+ return TrigExpression(self.name, subexpressions[1])
+end
+
+-- function TrigExpression:freeof(symbol)
+-- return self.expression:freeof(symbol)
+-- end
+
+-- function TrigExpression:substitute(map)
+-- for expression, replacement in pairs(map) do
+-- if self == expression then
+-- return replacement
+-- end
+-- end
+-- return TrigExpression(self.name, self.expression:substitute(map))
+-- end
+
+-- function TrigExpression:order(other)
+-- return self:tofunction():order(other)
+-- end
+
+-- function TrigExpression:tofunction()
+-- return FunctionExpression(self.name, {self.expression}, true)
+-- end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__TrigExpression.__index = FunctionExpression
+__TrigExpression.__call = TrigExpression.new
+TrigExpression = setmetatable(TrigExpression, __TrigExpression)
+
+----------------------
+-- Static constants --
+----------------------
+TrigExpression.NAMES = {sin=1, cos=2, tan=3, csc=4, sec=5, cot=6,
+ arcsin=7, arccos=8, arctan=9, arccsc=10, arcsec=11, arccot=12}
+
+TrigExpression.INVERSES = {sin="arcsin", cos="arccos", tan="arctan", csc="arccsc", sec="arcsec", cot="arccot",
+ arcsin="sin", arccos="cos", arctan="tan", arccsc="csc", arcsec="sec", arccot="cot"}
+
+TrigExpression.COSVALUES = {
+ ["0"] = Integer.one(),
+ ["1/6"] = sqrt(Integer(3))/Integer(2),
+ ["1/4"] = sqrt(Integer(2))/Integer(2),
+ ["1/3"] = Integer.one()/Integer(2),
+ ["1/2"] = Integer.zero(),
+ ["2/3"] = -Integer.one()/Integer(2),
+ ["3/4"] = -sqrt(Integer(2))/Integer(2),
+ ["5/6"] = -sqrt(Integer(3))/Integer(2),
+ ["1"] = -Integer.one(),
+ ["7/6"] = -sqrt(Integer(3))/Integer(2),
+ ["5/4"] = -sqrt(Integer(2))/Integer(2),
+ ["4/3"] = -Integer.one()/Integer(2),
+ ["3/2"] = Integer.zero(),
+ ["5/3"] = Integer.one()/Integer(2),
+ ["7/4"] = sqrt(Integer(2))/Integer(2),
+ ["11/6"] = sqrt(Integer(3))/Integer(2),
+}
+TrigExpression.ACOSVALUES = {
+ ["1"] = Integer.zero(),
+ ["(1/2 * sqrt(3,2))"] = PI * Integer(6) ^ Integer(-1),
+ ["(1/2 * sqrt(2,2))"] = PI * Integer(4) ^ Integer(-1),
+ ["1/2"] = PI * Integer(3) ^ Integer(-1),
+ ["0"] = PI * Integer(2) ^ Integer(-1),
+ ["-1/2"] = PI * Integer(2) * Integer(3) ^ Integer(-1),
+ ["(-1/2 * sqrt(2,2))"]= PI * Integer(3) * Integer(4) ^ Integer(-1),
+ ["(-1/2 * sqrt(3,2))"]= PI * Integer(5) * Integer(6) ^ Integer(-1),
+ ["-1"] = Integer(-1)*PI,
+}
+TrigExpression.ATANVALUES = {
+ ["(-1 * sqrt(3,2))"] = Integer(-1) * PI * Integer(3) ^ Integer(-1),
+ ["-1"] = Integer(-1) * PI * Integer(4) ^ Integer(-1),
+ ["(-1/3 * sqrt(3,2))"] = Integer(-1) * Integer(6) ^ Integer(-1),
+ ["0"] = Integer.zero(),
+ ["(1/3 * sqrt(3,2))"] = PI * Integer(6) ^ Integer(-1),
+ ["1"] = PI * Integer(4) ^ Integer(-1),
+ ["sqrt(3,2)"] = PI * Integer(3) ^ Integer(-1)
+}
+
+SIN = function (a)
+ return TrigExpression("sin", a)
+end
+
+COS = function (a)
+ return TrigExpression("cos", a)
+end
+
+TAN = function (a)
+ return TrigExpression("tan", a)
+end
+
+CSC = function (a)
+ return TrigExpression("csc", a)
+end
+
+SEC = function (a)
+ return TrigExpression("sec", a)
+end
+
+COT = function (a)
+ return TrigExpression("cot", a)
+end
+
+ARCSIN = function (a)
+ return TrigExpression("arcsin", a)
+end
+
+ARCCOS = function (a)
+ return TrigExpression("arccos", a)
+end
+
+ARCTAN = function (a)
+ return TrigExpression("arctan", a)
+end
+
+ARCCSC = function (a)
+ return TrigExpression("arccsc", a)
+end
+
+ARCSEC = function (a)
+ return TrigExpression("arcsec", a)
+end
+
+ARCCOT = function (a)
+ return TrigExpression("arccot", a)
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/calculus/_init.lua b/macros/luatex/latex/luacas/tex/calculus/_init.lua
new file mode 100644
index 0000000000..59ba1bbcc8
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/calculus/_init.lua
@@ -0,0 +1,6 @@
+-- Loads calculus files in the correct order.
+require("algebra._init")
+
+require("calculus.derivativeexpression")
+require("calculus.integralexpression")
+require("calculus.diffexpression")
diff --git a/macros/luatex/latex/luacas/tex/calculus/derivativeexpression.lua b/macros/luatex/latex/luacas/tex/calculus/derivativeexpression.lua
new file mode 100644
index 0000000000..b757759c4c
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/calculus/derivativeexpression.lua
@@ -0,0 +1,265 @@
+--- @class DerivativeExpression
+--- An expression for a single-variable derivative of an expression.
+--- @field symbol SymbolExpression
+--- @field expression Expression
+DerivativeExpression = {}
+__DerivativeExpression = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+-- Creates a new single-variable derivative operation with the given symbol and expression.
+--- @param expression Expression
+--- @param symbol Symbol
+--- @return DerivativeExpression
+function DerivativeExpression:new(expression, symbol)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ o.expression = Copy(expression)
+ o.symbol = symbol or SymbolExpression("x")
+
+ __o.__index = DerivativeExpression
+ __o.__tostring = function(a)
+ return '(d/d' .. tostring(a.symbol) .. " " .. tostring(a.expression) .. ')'
+ end
+ __o.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway
+ if not b:type() == DerivativeExpression then
+ return false
+ end
+ return a.symbol == b.symbol and a.expression == b.expression
+ end
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- @return Expression
+function DerivativeExpression:evaluate()
+ local exp = self.expression
+
+ -- The derivative of a constant is 0
+ if exp:isconstant() then
+ return Integer.zero()
+ end
+
+ -- The derivative of a symbol is either 1 or 0
+ if exp:type() == SymbolExpression then
+ if self.symbol == exp then
+ return Integer.one()
+ end
+ return Integer.zero()
+ end
+
+ -- Chain rule for arbitrary functions
+
+ if exp:type() == FunctionExpression then
+ local results = {}
+ for index,expression in ipairs(exp.expressions) do
+ local dout = FunctionExpression(exp.name,exp.expressions,exp.derivatives)
+ dout.variables = exp.variables
+ dout.derivatives[index] = dout.derivatives[index]+Integer.one()
+ local dinn = DerivativeExpression(expression,self.symbol):evaluate()
+ results[index] = dout*dinn
+ end
+ return BinaryOperation(BinaryOperation.ADD,results):autosimplify()
+ end
+
+ --if exp:type() == FunctionExpression then
+ -- local results = {}
+ -- for index,expression in ipairs(exp.expressions) do
+ -- local inn = DerivativeExpression(expression,self.symbol):autosimplify()
+ -- local out = Copy(exp)
+ -- out.orders[index] = out.orders[index] + Integer.one()
+ -- local result = inn*out
+ -- table.insert(results,result)
+ -- end
+ -- return BinaryOperation(BinaryOperation.ADD,results):autosimplify()
+ --end
+
+ --if exp:type() == FunctionExpression then
+ -- if exp.expressions[2] then
+ -- return self
+ -- end
+ -- return DerivativeExpression(exp.expressions[1], self.symbol) * FunctionExpression(exp.name, exp.expressions, exp.orders[1] + Integer.one(), exp.variables[1]):autosimplify()
+ --end
+
+ -- Chain rule for trig functions
+ if exp:type() == TrigExpression then
+ local internal = DerivativeExpression(exp.expression, self.symbol)
+
+ if exp.name == "sin" then
+ return (internal * COS(exp.expression)):autosimplify()
+ end
+ if exp.name == "cos" then
+ return (internal * -SIN(exp.expression)):autosimplify()
+ end
+ if exp.name == "tan" then
+ return (internal * SEC(exp.expression)^Integer(2)):autosimplify()
+ end
+ if exp.name == "csc" then
+ return (internal * -CSC(exp.expression)*COT(exp.expression)):autosimplify()
+ end
+ if exp.name == "sec" then
+ return (internal * -SEC(exp.expression)*TAN(exp.expression)):autosimplify()
+ end
+ if exp.name == "cot" then
+ return (internal * -CSC(exp.expression)^Integer(2)):autosimplify()
+ end
+ if exp.name == "arcsin" then
+ return (internal / (Integer(1)-exp.expression^Integer(2))^(Integer(1)/Integer(2))):autosimplify()
+ end
+ if exp.name == "arccos" then
+ return (-internal / (Integer(1)-exp.expression^Integer(2))^(Integer(1)/Integer(2))):autosimplify()
+ end
+ if exp.name == "arctan" then
+ return (internal / (Integer(1)+exp.expression^Integer(2))):autosimplify()
+ end
+ if exp.name == "arccsc" then
+ return (-internal / (ABS(exp.expression) * (Integer(1)-exp.expression^Integer(2))^(Integer(1)/Integer(2)))):autosimplify()
+ end
+ if exp.name == "arcsec" then
+ return (internal / (ABS(exp.expression) * (Integer(1)-exp.expression^Integer(2))^(Integer(1)/Integer(2)))):autosimplify()
+ end
+ if exp.name == "arccot" then
+ return (-internal / (Integer(1)+exp.expression^Integer(2))):autosimplify()
+ end
+ end
+
+ -- TODO: Piecewise functions
+ if self:type() == AbsExpression then
+ return DerivativeExpression(self.expression, self.symbol):autosimplify()
+ end
+
+ -- Uses linearity of derivatives to evaluate sum expressions
+ if exp.operation == BinaryOperation.ADD then
+ local parts = {}
+ for i, expression in pairs(exp.expressions) do
+ parts[i] = DerivativeExpression(expression, self.symbol)
+ end
+ return BinaryOperation(BinaryOperation.ADD, parts):autosimplify()
+ end
+
+ -- Uses product rule to evaluate product expressions
+ if exp.operation == BinaryOperation.MUL then
+ local sums = {}
+ for i, expression in pairs(exp.expressions) do
+ local products = {}
+ for j, innerexpression in pairs(exp.expressions) do
+ if i ~= j then
+ products[j] = innerexpression
+ else
+ products[j] = DerivativeExpression(innerexpression, self.symbol)
+ end
+ end
+ sums[i] = BinaryOperation(BinaryOperation.MUL, products)
+ end
+ return BinaryOperation(BinaryOperation.ADD, sums):autosimplify()
+ end
+
+ -- Uses the generalized power rule to evaluate power expressions
+ if exp.operation == BinaryOperation.POW then
+ local base = exp.expressions[1]
+ local exponent = exp.expressions[2]
+
+ return BinaryOperation.MULEXP({
+ BinaryOperation.POWEXP({base, exponent}),
+ BinaryOperation.ADDEXP({
+ BinaryOperation.MULEXP({
+ DD(base, self.symbol),
+ BinaryOperation.DIVEXP({exponent, base})}),
+ BinaryOperation.MULEXP({
+ DD(exponent, self.symbol),
+ LN(base)})})
+ }):autosimplify()
+ end
+
+ if exp:type() == Logarithm then
+ local base = exp.base
+ local expression = exp.expression
+
+ return BinaryOperation.SUBEXP({
+ BinaryOperation.DIVEXP({DD(expression, self.symbol),
+ BinaryOperation.MULEXP({expression, LN(base)})}),
+
+ BinaryOperation.DIVEXP({
+ BinaryOperation.MULEXP({LN(expression), DD(base, self.symbol)}),
+ BinaryOperation.MULEXP({BinaryOperation.POWEXP({LN(base), Integer(2)}), base})
+ })
+
+ }):autosimplify()
+ end
+
+ return self
+end
+
+--- @return Expression
+function DerivativeExpression:autosimplify()
+ return DerivativeExpression(self.expression:autosimplify(), self.symbol):evaluate()
+end
+
+--- @return table<number, Expression>
+function DerivativeExpression:subexpressions()
+ return {self.expression}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return DerivativeExpression
+function DerivativeExpression:setsubexpressions(subexpressions)
+ return DerivativeExpression(subexpressions[1], self.symbol)
+end
+
+-- function DerivativeExpression:freeof(symbol)
+-- return self.symbol.freeof(symbol) and self.expression:freeof(symbol)
+-- end
+
+-- Substitutes each expression for a new one.
+-- function DerivativeExpression:substitute(map)
+-- for expression, replacement in pairs(map) do
+-- if self == expression then
+-- return replacement
+-- end
+-- end
+-- -- Typically, we only perform substitution on autosimplified expressions, so this won't get called. May give strange results, i.e.,
+-- -- substituting and then evaluating the derivative may not return the same thing as evaluating the derivative and then substituting.
+-- return DerivativeExpression(self.expression:substitute(map), self.symbol)
+-- end
+
+--- @param other Expression
+--- @return boolean
+function DerivativeExpression:order(other)
+ if other:type() == IntegralExpression then
+ return true
+ end
+
+ if other:type() ~= DerivativeExpression then
+ return false
+ end
+
+ if self.symbol ~= other.symbol then
+ return self.symbol:order(other.symbol)
+ end
+
+ return self.expression:order(other.expression)
+end
+
+--- @return string
+function DerivativeExpression:tolatex()
+ return '\\frac{d}{d' .. self.symbol:tolatex() .. '}\\left(' .. self.expression:tolatex() .. '\\right)'
+end
+
+-----------------
+-- Inheritance --
+-----------------
+__DerivativeExpression.__index = CompoundExpression
+__DerivativeExpression.__call = DerivativeExpression.new
+DerivativeExpression = setmetatable(DerivativeExpression, __DerivativeExpression)
+
+----------------------
+-- Static constants --
+----------------------
+DD = function(expression, symbol)
+ return DerivativeExpression(expression, symbol)
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/calculus/diffexpression.lua b/macros/luatex/latex/luacas/tex/calculus/diffexpression.lua
new file mode 100644
index 0000000000..df1dc90f33
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/calculus/diffexpression.lua
@@ -0,0 +1,190 @@
+--- @class DiffExpression
+--- An expression for a multi-variable higher-order derivatives of an expression.
+--- @field symbols SymbolExpression
+--- @field expression Expression
+
+DiffExpression = {}
+__DiffExpression = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+-- Creates a new derivative operation with the given symbols and expression.
+--- @param expression Expression
+--- @param symbols table<number, Symbol>
+--- @return DiffExpression
+function DiffExpression:new(expression,symbols)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ o.symbols = Copy(symbols)
+ o.degree = #o.symbols
+ o.expression = Copy(expression)
+
+ __o.__tostring = function(a)
+ local varlist = '(d'
+ if a.degree == 1 then
+ varlist = varlist .. '/d' .. tostring(a.symbols[1]) .. " " .. tostring(a.expression) .. ')'
+ end
+ if a.degree > 1 then
+ varlist = varlist .. '^' .. tostring(a.degree) .. '/'
+ local varnum = 1
+ for index = 1, #a.symbols do
+ local var = a.symbols[#a.symbols-index+1]
+ if a.symbols[#a.symbols-index] == var then
+ varnum = varnum + 1
+ goto nextvar
+ end
+ if a.symbols[#a.symbols-index] ~=var then
+ if varnum == 1 then
+ varlist = varlist .. 'd' .. tostring(var)
+ else
+ varlist = varlist .. 'd' .. tostring(var) .. '^' .. tostring(varnum)
+ end
+ varnum = 1
+ end
+ ::nextvar::
+ end
+ varlist = varlist .. " " .. tostring(a.expression) .. ')'
+ end
+ return varlist
+ end
+
+ __o.__index = DiffExpression
+ __o.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always rungs this anyway
+ if not b:type() == DiffExpression then
+ return false
+ end
+ if a.expression ~= b.expression then
+ return false
+ end
+ local loc = 1
+ while a.symbols[loc] or b.symbols[loc] do
+ if not a.symbols[loc] or not b.symbols[loc] or
+ (a.symbols[loc] ~= b.symbols[loc]) then
+ return false
+ end
+ loc = loc + 1
+ end
+ return true
+ end
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- @return Expression
+function DiffExpression:evaluate()
+ local exp = self.expression
+
+ for _,var in ipairs(self.symbols) do
+ exp = DerivativeExpression(exp,var):evaluate()
+ end
+ return exp
+end
+
+--- @return Expression
+function DiffExpression:autosimplify()
+ return DiffExpression(self.expression:autosimplify(), self.symbols):evaluate()
+end
+
+
+--- @return table<number, Expression>
+function DiffExpression:subexpressions()
+ return {self.expression}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return DiffExpression
+function DiffExpression:setsubexpressions(subexpressions)
+ return DiffExpression(subexpressions[1], self.symbols)
+end
+
+--- @param other Expression
+--- @return boolean
+function DiffExpression:order(other)
+ if other:type() == IntegralExpression then
+ return true
+ end
+
+ if other:type() ~= DiffExpression then
+ return false
+ end
+
+ if self.degree > other.degree then
+ return false
+ end
+
+ if self.degree < other.degree then
+ return true
+ end
+
+ return self.expression:order(other.expression)
+end
+
+--- @return string
+function DiffExpression:tolatex()
+ local varlist = '\\frac{'
+ if self.degree == 1 then
+ varlist = varlist .. 'd}{d' .. self.symbols[1]:tolatex() .. '}\\left(' .. self.expression:tolatex() .. '\\right)'
+ end
+ if self.degree > 1 then
+ local cvarlist = {}
+ local count = 1
+ for index, var in ipairs(self.symbols) do
+ if var == self.symbols[index+1] then
+ count = count + 1
+ else
+ table.insert(cvarlist,{var,count})
+ count = 1
+ end
+ end
+ if #cvarlist == 1 then
+ varlist = varlist .. 'd^{' .. self.degree .. '}}{' .. 'd' .. cvarlist[1][1]:tolatex() .. '^{' .. self.degree .. '}'
+ end
+ if #cvarlist > 1 then
+ varlist = varlist .. '\\partial^{' .. self.degree .. '}}{'
+ for index, varnum in ipairs(cvarlist) do
+ var = cvarlist[#cvarlist - index+1][1]
+ num = cvarlist[#cvarlist - index+1][2]
+ if num == 1 then
+ varlist = varlist .. '\\partial ' .. var:tolatex()
+ else
+ varlist = varlist .. '\\partial ' .. var:tolatex() .. '^{' .. num .. '}'
+ end
+ end
+ end
+ varlist = varlist .. '} \\left(' .. self.expression:tolatex() .. '\\right)'
+ end
+ return varlist
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__DiffExpression.__index = CompoundExpression
+__DiffExpression.__call = DiffExpression.new
+DiffExpression = setmetatable(DiffExpression, __DiffExpression)
+
+----------------------
+-- Static constants --
+----------------------
+
+diff = function(expression,...)
+ local symbols = {}
+ for i = 1, select("#",...) do
+ local var = select(i,...)
+ if #var == 0 then
+ table.insert(symbols,var)
+ end
+ if #var > 0 then
+ for index=1, RR(var[2]) do
+ table.insert(symbols,var[1])
+ end
+ end
+ end
+ return DiffExpression(expression,symbols)
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/calculus/integralexpression.lua b/macros/luatex/latex/luacas/tex/calculus/integralexpression.lua
new file mode 100644
index 0000000000..5267127264
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/calculus/integralexpression.lua
@@ -0,0 +1,934 @@
+--- @class IntegralExpression
+--- An expression for the integral of an expression.
+--- @field symbol SymbolExpression
+--- @field expression Expression
+--- @field upper Expression
+--- @field lower Expression
+--- @field attempts table<number, Expression>
+--- @field results table<number, Expression>
+--- @field enhancedsubstitution Integer
+--- @field recursive boolean
+
+IntegralExpression = {}
+__IntegralExpression = {}
+
+
+--------------------------
+-- Static functionality --
+--------------------------
+
+--- Recursive part of the indefinite integral operator. Returns nil if the expression could not be integrated.
+--- We switch to prodcedural programming here because it is more natural.
+--- @param integral IntegralExpression
+--- @return Expression|nil
+function IntegralExpression.integrate(integral)
+ integral.expression = integral.expression:autosimplify()
+
+ if not integral.recursive and #integral.attempts > 0 then
+ return Copy(integral:lock(Expression.NIL, true))
+ end
+
+ -- print(integral.expression)
+
+ local F = IntegralExpression.table(integral)
+ if F then return F end
+
+ -- If we see the same integrand again, and hasn't been solved already, then the integral can't be solved
+ local resultindex = Contains(integral.attempts, integral.expression)
+ if resultindex then
+ return integral.results[resultindex]
+ end
+ local newindex = #integral.attempts+1
+ integral.attempts[newindex] = integral.expression
+
+ -- print("Evalutaing: " .. tostring(integral.expression))
+
+ F = IntegralExpression.linearproperties(integral)
+ if F then
+ -- print("Linear Properties")
+ integral.results[newindex] = F
+ return F
+ end
+
+ -- local exp = integral.expression
+ -- local sym = integral.symbol
+ -- local es = integral.enhancedsubstitution
+ -- integral.enhancedsubstitution = Integer.zero()
+ F = IntegralExpression.substitutionmethod(integral)
+ if F then
+ -- print("u-Substitution")
+ integral.results[newindex] = F
+ return F
+ end
+ -- integral.expression = exp
+ -- integral.symbol = sym
+ -- integral.enhancedsubstitution = es
+
+
+ F = IntegralExpression.rationalfunction(integral)
+ if F then
+ -- print("Rational Function")
+ integral.results[newindex] = F
+ return F
+ end
+
+ F = IntegralExpression.partsmethod(integral)
+ if F then
+ -- print("Parts")
+ integral.results[newindex] = F
+ return F
+ end
+
+ F = IntegralExpression.eulersformula(integral)
+ if F then
+ -- print("Euler's formula")
+ integral.results[newindex] = F
+ return F
+ end
+
+ local expanded = integral.expression:expand()
+ if integral.expression ~= expanded then
+ integral.expression = expanded
+ F = IntegralExpression.integrate(integral)
+ if F then
+ -- print("Expanded")
+ integral.results[newindex] = F
+ return F
+ end
+ end
+
+ expanded = (Integer.one()/((Integer.one()/integral.expression):autosimplify():expand())):autosimplify()
+ if integral.expression ~= expanded then
+ integral.expression = expanded
+ F = IntegralExpression.integrate(integral)
+ if F then
+ -- print("Inverse Expanded")
+ integral.results[newindex] = F
+ return F
+ end
+ end
+
+ F = IntegralExpression.enhancedsubstitutionmethod(integral)
+ if F then
+ -- print("Enhanced u-Substitution")
+ integral.results[newindex] = F
+ return F
+ end
+
+ return nil
+end
+
+--- A table of basic integrals, returns nil if the integrand isn't in the table.
+--- @param integral IntegralExpression
+--- @return Expression|nil
+function IntegralExpression.table(integral)
+ local integrand = integral.expression
+ local symbol = integral.symbol
+
+ -- Constant integrand rule - int(c, x) = c*x
+ if integrand:freeof(symbol) then
+ return integrand*symbol
+ end
+
+ if integrand:type() == SymbolExpression then
+
+ -- int(x, x) = x^2/2
+ if integrand == symbol then
+ return integrand ^ Integer(2) / Integer(2)
+ end
+
+ -- Constant integrand rule again
+ return integrand*symbol
+ end
+
+ if integrand:type() == BinaryOperation then
+
+ if integrand.operation == BinaryOperation.POW then
+ -- int(1/x, x) = ln(x)
+ if integrand.expressions[1] == symbol and integrand.expressions[2] == Integer(-1) then
+ return LN(symbol)
+ end
+
+ -- Cavalieri's formula - int(x^n, x) = x^(n+1)/(n+1)
+ if integrand.expressions[1] == symbol and integrand.expressions[2]:freeof(symbol) then
+ return symbol ^ (integrand.expressions[2] + Integer.one()) / (integrand.expressions[2] + Integer.one())
+ end
+
+ -- int(n^x, x) = n^x/ln(n)
+ if integrand.expressions[1]:freeof(symbol) and integrand.expressions[2] == symbol then
+ return integrand / LN(integrand.expressions[1])
+ end
+
+ -- int(csc(x)^2, x) = -cot(x)
+ if integrand.expressions[1] == CSC(symbol) and integrand.expressions[2] == Integer(2) then
+ return -COT(symbol)
+ end
+
+ -- int(sec(x)^2, x) = tan(x)
+ if integrand.expressions[1] == SEC(symbol) and integrand.expressions[2] == Integer(2) then
+ return TAN(symbol)
+ end
+ end
+
+ if integrand.operation == BinaryOperation.MUL and #integrand.expressions == 2 then
+ -- int(tan(x)sec(x), x) = sec(x)
+ if integrand.expressions[1] == TAN(symbol) and integrand.expressions[2] == SEC(symbol) then
+ return SEC(symbol)
+ end
+
+ -- int(csc(x)cot(x), x) = -csc(x)
+ if integrand.expressions[1] == CSC(symbol) and integrand.expressions[2] == COT(symbol) then
+ return -CSC(symbol)
+ end
+ end
+
+ return nil
+ end
+
+ if integrand:type() == Logarithm then
+ -- int(log_n(x), x) = (x*ln(x)-x)/ln(n)
+ if integrand.base:freeof(symbol) and integrand.expression == symbol then
+ return (symbol * LN(symbol) - symbol) / LN(integrand.base)
+ end
+
+ return nil
+ end
+
+ if integrand:type() == TrigExpression then
+ if integrand == SIN(symbol) then
+ return -COS(symbol)
+ end
+
+ if integrand == COS(symbol) then
+ return SIN(symbol)
+ end
+
+ if integrand == TAN(symbol) then
+ return -LN(COS(symbol))
+ end
+
+ if integrand == CSC(symbol) then
+ return -LN(CSC(symbol)+COT(symbol))
+ end
+
+ if integrand == SEC(symbol) then
+ return LN(SEC(symbol) + TAN(symbol))
+ end
+
+ if integrand == COT(symbol) then
+ return LN(SIN(symbol))
+ end
+
+ if integrand == ARCSIN(symbol) then
+ return symbol*ARCSIN(symbol) + (Integer.one()-symbol^(Integer(2)))^(Integer.one()/Integer(2))
+ end
+
+ if integrand == ARCCOS(symbol) then
+ return symbol*ARCCOS(symbol) - (Integer.one()-symbol^(Integer(2)))^(Integer.one()/Integer(2))
+ end
+
+ if integrand == ARCTAN(symbol) then
+ return symbol*ARCTAN(symbol) - (Integer.one()/Integer(2))*LN(Integer.one()+symbol^Integer(2))
+ end
+
+ if integrand == ARCCSC(symbol) then
+ return symbol*ARCCSC(symbol) + LN(symbol*(Integer.one()+(Integer.one()-symbol^(Integer(-2)))^(Integer.one()/Integer(2))))
+ end
+
+ if integrand == ARCSEC(symbol) then
+ return symbol*ARCSEC(symbol) - LN(symbol*(Integer.one()+(Integer.one()-symbol^(Integer(-2)))^(Integer.one()/Integer(2))))
+ end
+
+ if integrand == ARCCOT(symbol) then
+ return symbol*ARCCOT(symbol) + (Integer.one()/Integer(2))*LN(Integer.one()+symbol^Integer(2))
+ end
+ end
+
+ return nil
+end
+
+--- Uses linearity to break up the integral and integrate each piece.
+--- @param integral IntegralExpression
+--- @return Expression|nil
+function IntegralExpression.linearproperties(integral)
+ local expression = integral.expression
+ local symbol = integral.symbol
+ local es = integral.enhancedsubstitution
+
+ if expression:type() == BinaryOperation then
+ if expression.operation == BinaryOperation.MUL then
+ local freepart = Integer.one()
+ local variablepart = Integer.one()
+ for _, term in ipairs(expression.expressions) do
+ if term:freeof(symbol) then
+ freepart = freepart*term
+ else
+ variablepart = variablepart*term
+ end
+ end
+ if freepart == Integer.one() then
+ return nil
+ end
+ integral.expression = variablepart
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ local F = IntegralExpression.integrate(integral)
+ if F then
+ return freepart*F
+ end
+ return nil
+ end
+
+ if expression.operation == BinaryOperation.ADD then
+ local sum = Integer.zero()
+ for _, term in ipairs(expression.expressions) do
+ integral.expression = term
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ local F = IntegralExpression.integrate(integral)
+ if F then
+ sum = sum + F
+ else
+ return nil
+ end
+
+ end
+ return sum
+ end
+ end
+
+ return nil
+end
+
+--- Attempts u-substitutions to evaluate the integral.
+--- @param integral IntegralExpression
+--- @return Expression|nil
+function IntegralExpression.substitutionmethod(integral)
+ local expression = integral.expression
+ local symbol = integral.symbol
+ local es = integral.enhancedsubstitution
+
+ local P = IntegralExpression.trialsubstitutions(expression)
+ local F = nil
+ local i = 1
+
+ while not F and i <= #P do
+ local g = P[i]
+ if g ~= symbol and not g:freeof(symbol) then
+ local subsymbol = SymbolExpression("u")
+ if symbol == SymbolExpression("u") then
+ subsymbol = SymbolExpression("v")
+ end
+ local u = (expression / (DerivativeExpression(g, symbol))):autosimplify()
+ u = u:substitute({[g]=subsymbol}):autosimplify()
+
+ --factor u and cancel like non-constant terms
+ u = u:factor():autosimplify()
+
+ if u:freeof(symbol) then
+ integral.expression = u
+ integral.symbol = subsymbol
+ integral.enhancedsubstitution = es
+ F = IntegralExpression.integrate(integral)
+ if F then
+ if integral.recursive then
+ F = F:substitute({[subsymbol]=g})
+ end
+ return F
+ end
+ end
+ end
+ i = i + 1
+ end
+
+ return F
+end
+
+--- Attempts u-substitutions to evaluate the integral, including solving for the original variable and substituting the result into the expression.
+--- @param integral IntegralExpression
+--- @return Expression|nil
+function IntegralExpression.enhancedsubstitutionmethod(integral)
+ local expression = integral.expression
+ local symbol = integral.symbol
+ local es = integral.enhancedsubstitution
+
+ local P = IntegralExpression.trialsubstitutions(expression)
+ local F = nil
+ local i = 1
+
+ while not F and i <= #P do
+ local g = P[i]
+ if g ~= symbol and not g:freeof(symbol) then
+ local subsymbol = SymbolExpression("u")
+ if symbol == SymbolExpression("u") then
+ subsymbol = SymbolExpression("v")
+ end
+ local u = (expression / (DerivativeExpression(g, symbol))):autosimplify()
+ u = u:substitute({[g]=subsymbol}):autosimplify()
+
+ --factor u and cancel like non-constant terms
+ u = u:factor():autosimplify()
+
+ if integral.enhancedsubstitution > Integer.zero() then
+ local f = Equation(subsymbol, g):solvefor(symbol)
+ if f.lhs == symbol then
+ u = u:substitute({[symbol]=f.rhs}):autosimplify()
+ integral.expression = u
+ integral.symbol = subsymbol
+ integral.enhancedsubstitution = integral.enhancedsubstitution - Integer.one()
+ F = IntegralExpression.integrate(integral)
+ if F then
+ if integral.recursive then
+ F = F:substitute({[subsymbol]=g})
+ end
+ return F
+ end
+ integral.enhancedsubstitution = integral.enhancedsubstitution + Integer.one()
+ end
+ end
+ end
+ i = i + 1
+ end
+
+ return F
+end
+
+--- Generates a list of possible u-substitutions to attempt
+--- @param expression Expression
+--- @return table<number, Expression>
+function IntegralExpression.trialsubstitutions(expression)
+ local substitutions = {}
+
+ -- Recursive part - evaluates each term in a product.
+ if expression:type() == BinaryOperation and expression.operation == BinaryOperation.MUL then
+ substitutions[#substitutions+1] = expression
+ for _, term in ipairs(expression.expressions) do
+ substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(term))
+ end
+ end
+
+ --Recursive part - evaluates each term in a sum.
+ if expression:type() == BinaryOperation and expression.operation == BinaryOperation.ADD then
+ substitutions[#substitutions+1] = expression
+ for _,term in ipairs(expression.expressions) do
+ substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(term))
+ end
+ end
+
+ -- Function forms and arguments of function forms (includes a recursive part)
+ if expression:type() == TrigExpression or expression:type() == Logarithm then
+ substitutions[#substitutions+1] = expression
+ if not expression.expression:isatomic() then
+ substitutions[#substitutions+1] = expression.expression
+ end
+ substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(expression.expression))
+ end
+
+ -- Bases and exponents of powers
+ if expression:type() == BinaryOperation and expression.operation == BinaryOperation.POW then
+ substitutions[#substitutions+1] = expression
+ -- Atomic expressions are technically valid substitutions, but they won't be useful
+ if not expression.expressions[1]:isatomic() then
+ --substitutions[#substitutions+1] = expression.expressions[1]
+ substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(expression.expressions[1]))
+ end
+ if not expression.expressions[2]:isatomic() then
+ --substitutions[#substitutions+1] = expression.expressions[2]
+ substitutions = JoinArrays(substitutions, IntegralExpression.trialsubstitutions(expression.expressions[2]))
+ end
+ end
+
+ return substitutions
+end
+
+
+--- Uses Lazard, Rioboo, Rothstein, and Trager's method to integrate rational functions.
+--- This is mostly to try to avoid factoring and finding the roots of the full denominator whenever possible.
+--- @param integral IntegralExpression
+--- @return Expression|nil
+function IntegralExpression.rationalfunction(integral)
+ local expression = integral.expression
+ local symbol = integral.symbol
+ local es = integral.enhancedsubstitution
+
+ -- Type checking and conversion to polynomial type.
+ local f, g, fstat, gstat
+ if expression:type() == BinaryOperation and expression.operation == BinaryOperation.POW and expression.expressions[2] == Integer(-1) then
+ g, gstat = expression.expressions[1]:topolynomial()
+ if not gstat then
+ return nil
+ end
+ f = PolynomialRing({Integer.one()}, g.symbol)
+ else
+ if expression:type() ~= BinaryOperation or expression.operation ~= BinaryOperation.MUL or expression.expressions[3] then
+ return nil
+ end
+ if expression.expressions[2]:type() == BinaryOperation and expression.expressions[2].operation == BinaryOperation.POW and expression.expressions[2].expressions[2] == Integer(-1) then
+ if expression.expressions[1].topolynomial ~=nil and expression.expressions[2].expressions[1].topolynomial ~=nil then
+ f, fstat = expression.expressions[1]:topolynomial()
+ g, gstat = expression.expressions[2].expressions[1]:topolynomial()
+ end
+ elseif expression.expressions[1]:type() == BinaryOperation and expression.expressions[1].operation == BinaryOperation.POW and expression.expressions[1].expressions[2] == Integer(-1) then
+ if expression.expressions[2].topolynomial ~= nil and expression.expressions[1].expressions[1].topolynomial ~= nil then
+ f, fstat = expression.expressions[2]:topolynomial()
+ g, gstat = expression.expressions[1].expressions[1]:topolynomial()
+ end
+ else
+ return nil
+ end
+
+ if not fstat or not gstat or f.symbol ~= symbol.symbol or g.symbol ~= symbol.symbol then
+ return nil
+ end
+ end
+
+ -- Explicit handling of degree 1 or less over a binomial.
+ do
+ local disc = g.coefficients[1]*g.coefficients[1]-Integer(4)*g.coefficients[2]*g.coefficients[0]
+ if f.degree <= Integer.one() and g.degree == Integer(2) and disc < Integer.zero() then
+ return (f.coefficients[1] * LN(g.coefficients[0] + g.coefficients[1] * symbol + g.coefficients[2] * symbol ^ Integer(2))/(Integer(2) * g.coefficients[2]) + (Integer(2)*f.coefficients[0]*g.coefficients[2] - f.coefficients[1]*g.coefficients[1]) / (g.coefficients[2] * sqrt(Integer(4)*g.coefficients[0]*g.coefficients[2] - g.coefficients[1] ^ Integer(2))) * ARCTAN((Integer(2)*g.coefficients[2]*symbol+g.coefficients[1]) / sqrt(Integer(4)*g.coefficients[0]*g.coefficients[2]-g.coefficients[1] ^ Integer(2)))):autosimplify()
+ end
+ end
+
+ -- If the polynomials are not relatively prime, divides out the common factors.
+ local gcd = PolynomialRing.gcd(f, g)
+ if gcd ~= Integer.one() then
+ f, g = f // gcd, g // gcd
+ end
+
+ -- Seperates out the polynomial part and rational part and integrates the polynomial part.
+ local q, h = f:divremainder(g)
+ integral.expression = q
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ U = IntegralExpression.integrate(integral)
+
+ if h == Integer.zero() then
+ return U
+ end
+
+ -- Performs partial fraction decomposition into square-free denominators on the rational part.
+ local gg = g:squarefreefactorization()
+ local pfd = PolynomialRing.partialfractions(h, g, gg)
+
+ -- Hermite reduction.
+ local V = Integer.zero()
+ for _, term in ipairs(pfd.expressions) do
+ local i = #term.expressions
+ if i > 1 then
+ for j = 1, i-1 do
+ local n = term.expressions[j].expressions[1]
+ local d = term.expressions[j].expressions[2].expressions[1]
+ local p = term.expressions[j].expressions[2].expressions[2]
+
+ local _, s, t = PolynomialRing.extendedgcd(d, d:derivative())
+ s = s * n
+ t = t * n
+ V = V - t / ((p-Integer.one()) * BinaryOperation.POWEXP({d, p-Integer.one()}))
+ term.expressions[j+1].expressions[1] = term.expressions[j+1].expressions[1] + s + t:derivative() / (p-Integer.one())
+ end
+ end
+ end
+
+ --Lazard-Rioboo-Trager method.
+ local W = Integer.zero()
+ for _, term in ipairs(pfd.expressions) do
+ local a = term.expressions[#term.expressions].expressions[1]
+ local b = term.expressions[1].expressions[2].expressions[1]
+ local y = a - b:derivative() * PolynomialRing({Integer.zero(), Integer.one()}, "_")
+ local r = PolynomialRing.resultant(b, y)
+
+
+ local rr = r:squarefreefactorization()
+ local remainders = PolynomialRing.monicgcdremainders(b, y)
+ for pos, factor in ipairs(rr.expressions) do
+ if pos > 1 then
+ local re = factor.expressions[1]
+ local e = factor.expressions[2]
+ local roots = re:roots()
+ for _, root in ipairs(roots) do
+ local w
+ for _, remainder in ipairs(remainders) do
+ if remainder.degree == e then
+ w = remainder
+ break
+ end
+ end
+ W = W + root*LN(w:substitute({[SymbolExpression("_")] = root}))
+ end
+ end
+ end
+ end
+
+ return U + V + W
+end
+
+
+--- Attempts integration by parts for expressions with a polynomial factor in them. Other product expressions use Euler's formula.
+--- @param integral IntegralExpression
+--- @return Expression|nil
+function IntegralExpression.partsmethod(integral)
+ local expression = integral.expression
+ local symbol = integral.symbol
+ local es = integral.enhancedsubstitution
+
+ if expression:type() ~= BinaryOperation or expression.operation ~= BinaryOperation.MUL then
+ return
+ end
+
+ local u
+ local vp = Integer.one()
+ --looking for ILATE
+ for _, exp in ipairs(expression:subexpressions()) do
+ if exp:type() == TrigExpression and (exp.name == "arctan" or exp.name == "arccos" or exp.name == "arcsin" or exp.name == "arccot" or exp.name == "arcsec" or exp.name == "arccsc") then
+ u = exp
+ else
+ vp = vp * exp
+ end
+ end
+
+ if not u or u:freeof(symbol) then
+ goto skipI
+ else
+ vp = vp:autosimplify()
+ end
+
+ --if vp:type() == Logarithm or vp.topolynomial or (vp:type() == TrigExpression and (vp.name == "cos" or vp.name == "sin")) or (vp.operation == BinaryOperation.POW and vp.expressions[1]:freeof(symbol)) then
+ if select(2,vp:topolynomial()) then
+ integral.expression = vp
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ local v = IntegralExpression.integrate(integral)
+ if not v then
+ goto skipI
+ end
+
+ local up = DerivativeExpression(u, symbol):autosimplify()
+
+ integral.expression = v*up
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ local vup = IntegralExpression.integrate(integral)
+ if not vup then
+ goto skipI
+ end
+
+ local result = u*v - vup
+
+ return result:autosimplify()
+ end
+ ::skipI::
+
+ local u
+ local vp = Integer.one()
+ --looking for LATE
+ for _, exp in ipairs(expression:subexpressions()) do
+ if exp:type() == Logarithm then
+ u = exp
+ else
+ vp = vp * exp
+ end
+ end
+
+ if not u or u:freeof(symbol) then
+ goto skipL
+ else
+ vp = vp:autosimplify()
+ end
+
+ --if vp.topolynomial or (vp:type() == TrigExpression and (vp.name == "cos" or vp.name == "sin")) or (vp.operation == BinaryOperation.POW and vp.expressions[1]:freeof(symbol)) then
+ if select(2,vp:topolynomial()) then
+ integral.expression = vp
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ local v = IntegralExpression.integrate(integral)
+ if not v then
+ goto skipL
+ end
+
+ local up = DerivativeExpression(u, symbol):autosimplify()
+
+ integral.expression = v*up
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ local vup = IntegralExpression.integrate(integral)
+ if not vup then
+ goto skipL
+ end
+
+ local result = u*v - vup
+
+ return result:autosimplify()
+ end
+ ::skipL::
+
+ local u
+ local vp = Integer.one()
+ --looking for ATE
+ for _, exp in ipairs(expression:subexpressions()) do
+ local _, bool = exp:topolynomial()
+ if bool then
+ u = exp
+ else
+ vp = vp * exp
+ end
+ end
+
+ if not u or u:freeof(symbol) then
+ return
+ else
+ vp = vp:autosimplify()
+ end
+
+ if (vp:type() == TrigExpression and (vp.name == "cos" or vp.name == "sin")) or (vp.operation == BinaryOperation.POW and vp.expressions[1]:freeof(symbol)) then
+ local results = {}
+ while u ~= Integer.zero() do
+ integral.expression = vp
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ local v = IntegralExpression.integrate(integral):unlock():autosimplify()
+ if not v then
+ return
+ end
+ local up = DerivativeExpression(u, symbol):autosimplify()
+
+ if not integral.recursive then
+ return (u*v - IntegralExpression(v*up, symbol):nonrecursive():lock(Expression.NIL, true)):autosimplify()
+ end
+
+ results[#results+1] = u*v
+ u = up
+ vp = v
+ end
+
+ local result = results[#results]
+ for i=#results-1,1,-1 do
+ result = results[i] - result
+ end
+
+ return result:autosimplify()
+ end
+end
+
+--- Attempts integration using Euler's formula and kind. Alternative for integration by parts for many expressions.
+--- @param integral IntegralExpression
+--- @return Expression|nil
+function IntegralExpression.eulersformula(integral)
+ local expression = integral.expression
+ local symbol = integral.symbol
+ local es = integral.enhancedsubstitution
+
+ local new = expression:substitute({[COS(symbol)] = (E^(I*symbol) + E^(-I*symbol))/Integer(2),
+ [SIN(symbol)] = (E^(I*symbol) - E^(-I*symbol))/(Integer(2)*I)})
+
+ if new == expression then
+ return
+ end
+
+ integral.expression = new:autosimplify():expand()
+ integral.symbol = symbol
+ integral.enhancedsubstitution = es
+ local complexresult = IntegralExpression.integrate(integral)
+ if not complexresult then
+ return
+ end
+
+ -- TODO: Proper complex number conversion methods
+ local function converttorectangular(exp)
+ exp = exp:expand()
+ local results = {}
+ for index, sub in ipairs(exp:subexpressions()) do
+ results[index] = converttorectangular(sub)
+ end
+ local converted = exp:setsubexpressions(results)
+
+ if converted.operation == BinaryOperation.POW and converted.expressions[1] == E and converted.expressions[2].operation == BinaryOperation.MUL then
+ local ipart
+ local rest = Integer.one()
+ for _, factor in ipairs(converted.expressions[2]:subexpressions()) do
+ if factor == I then
+ ipart = true
+ else
+ rest = rest * factor
+ end
+ end
+ if ipart then
+ return (COS(rest) + I*SIN(rest)):autosimplify()
+ end
+ end
+
+ return converted
+ end
+
+ return converttorectangular(complexresult:autosimplify()):expand():autosimplify()
+
+end
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new integral operation with the given symbol and expression.
+--- @param expression Expression
+--- @param symbol SymbolExpression
+--- @param lower Expression
+--- @param upper Expression
+function IntegralExpression:new(expression, symbol, lower, upper)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ if not symbol or not expression then
+ error("Send wrong number of parameters: integrals must have a variable to integrate with respect to and an expression to integrate.")
+ end
+
+ if lower and not upper then
+ error("Send wrong number of parameters: definite integrals must have an upper and a lower bound.")
+ end
+
+ o.symbol = symbol
+ o.expression = Copy(expression)
+ o.upper = Copy(upper)
+ o.lower = Copy(lower)
+ o.recursive = true
+
+ o.attempts = {}
+ o.results = {}
+ o.enhancedsubstitution = IntegralExpression.ENHANCEDSUBSTITUTIONRECURSIONLIMIT
+
+ __o.__index = IntegralExpression
+ __o.__tostring = function(a)
+ if a:isdefinite() then
+ return 'int(' .. tostring(a.expression) .. ", " .. tostring(a.symbol) .. ", ".. tostring(a.lower) .. ', ' .. tostring(a.upper) .. ')'
+ end
+ return 'int(' .. tostring(a.expression) .. ", " .. tostring(a.symbol) .. ')'
+ end
+ __o.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always rungs this anyway
+ if not b:type() == IntegralExpression then
+ return false
+ end
+ return a.symbol == b.symbol and a.expression == b.expression and a.upper == b.upper and a.lower == b.lower
+ end
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- Returns true if the integral is definite, i.e., has an upper and lower bound.
+--- @return boolean
+function IntegralExpression:isdefinite()
+ return self.upper ~= nil
+end
+
+--- Sets the integral to not autosimplify other integral expressions that are produced by the integration process.
+--- THIS METHOD MUTATES THE OBJECT IT IS CALLED ON.
+function IntegralExpression:nonrecursive()
+ self.recursive = false
+ return self
+end
+
+--- @return Expression
+function IntegralExpression:autosimplify()
+ local arg = IntegralExpression(self.expression, self.symbol)
+ local integrated = IntegralExpression.integrate(arg)
+
+ -- Our expression could not be integrated.
+ if not integrated then
+ return self
+ end
+
+ if not self.recursive then
+ return integrated:autosimplify():unlock(true)
+ end
+
+ if self:isdefinite() then
+ return (integrated:substitute({[self.symbol]=self.upper}) - integrated:substitute({[self.symbol]=self.lower})):autosimplify()
+ end
+
+ return integrated:autosimplify()
+end
+
+
+--- @return table<number, Expression>
+function IntegralExpression:subexpressions()
+ if self:isdefinite() then
+ return {self.expression, self.symbol, self.lower, self.upper}
+ end
+
+ return {self.expression, self.symbol}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return IntegralExpression
+function IntegralExpression:setsubexpressions(subexpressions)
+ local out = IntegralExpression(subexpressions[1], subexpressions[2], subexpressions[3], subexpressions[4])
+
+ return out;
+end
+
+-- function IntegralExpression:freeof(symbol)
+-- if self:isdefinite() then
+-- return self.expression:freeof(symbol) and self.upper:freeof(symbol) and self.lower:freeof(symbol)
+-- end
+-- return self.expression:freeof(symbol)
+-- end
+
+-- -- Substitutes each expression for a new one.
+-- function IntegralExpression:substitute(map)
+-- for expression, replacement in pairs(map) do
+-- if self == expression then
+-- return replacement
+-- end
+-- end
+-- -- Typically, we only perform substitution on autosimplified expressions, so this won't get called. May give strange results, i.e.,
+-- -- substituting and then evaluating the integral may not return the same thing as evaluating the integral and then substituting.
+-- if self:isdefinite() then
+-- return IntegralExpression(self.symbol, self.expression:substitute(map), self.upper:substitute(map), self.lower:substitute(map))
+-- end
+-- return IntegralExpression(self.symbol, self.expression:substitute(map))
+-- end
+
+--- @param other Expression
+--- @return boolean
+function IntegralExpression:order(other)
+ if other:type() ~= IntegralExpression then
+ return false
+ end
+
+ if self.symbol ~= other.symbol then
+ return self.symbol:order(other.symbol)
+ end
+
+ return self.expression:order(other.expression)
+end
+
+--- @return string
+function IntegralExpression:tolatex()
+ if self:isdefinite() then
+ return '\\int_{' .. self.lower:tolatex() .. '}^{' .. self.upper:tolatex() .. '}{' .. self.expression:tolatex() .. '\\mathop{d' .. self.symbol:tolatex() .. '}}'
+ end
+ return '\\int{' .. self.expression:tolatex() .. '\\mathop{d' .. self.symbol:tolatex() .. '}}'
+end
+
+
+-----------------
+-- Inheritance --
+-----------------
+__IntegralExpression.__index = CompoundExpression
+__IntegralExpression.__call = IntegralExpression.new
+IntegralExpression = setmetatable(IntegralExpression, __IntegralExpression)
+
+----------------------
+-- Static constants --
+----------------------
+INT = function(symbol, expression, lower, upper)
+ return IntegralExpression(symbol, expression, lower, upper)
+end
+
+----------------------
+-- Static constants --
+----------------------
+
+-- Limit for the maximum number of full u-subs to attempts for any integral.
+-- This should be low, since integrals are highly unlikely to need more than 1 or 2 u-subs, and gives exponentially worse performance the higher the number is.
+IntegralExpression.ENHANCEDSUBSTITUTIONRECURSIONLIMIT = Integer(2) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/_init.lua b/macros/luatex/latex/luacas/tex/core/_init.lua
new file mode 100644
index 0000000000..cfd640a214
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/_init.lua
@@ -0,0 +1,16 @@
+-- Loads core files in the correct order.
+
+require("core.expression")
+require("core.atomicexpression")
+require("core.compoundexpression")
+require("core.constantexpression")
+require("core.symbolexpression")
+require("core.binaryoperation")
+require("core.functionexpression")
+
+
+require("core.binaryoperation.power")
+require("core.binaryoperation.product")
+require("core.binaryoperation.sum")
+require("core.binaryoperation.quotient")
+require("core.binaryoperation.difference") \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/atomicexpression.lua b/macros/luatex/latex/luacas/tex/core/atomicexpression.lua
new file mode 100644
index 0000000000..6cc6e4f58e
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/atomicexpression.lua
@@ -0,0 +1,70 @@
+--- @class AtomicExpression
+--- Interface for an atomic mathematical expression that has no sub-expressions.
+AtomicExpression = {}
+__AtomicExpression = {}
+
+
+----------------------
+-- Required methods --
+----------------------
+
+--- Converts an atomic expression to its equivalent compound expression, if it has one.
+--- @return Expression
+function AtomicExpression:tocompoundexpression()
+ return self
+end
+
+----------------------
+-- Instance methods --
+----------------------
+
+--- @return AtomicExpression
+function AtomicExpression:evaluate()
+ return self
+end
+
+--- @return AtomicExpression
+function AtomicExpression:autosimplify()
+ return self
+end
+
+--- @return table<number, Expression>
+function AtomicExpression:subexpressions()
+ return {}
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return AtomicExpression
+function AtomicExpression:setsubexpressions(subexpressions)
+ return self
+end
+
+--- @param map table<Expression, Expression>
+--- @return Expression
+function AtomicExpression:substitute(map)
+ for expression, replacement in pairs(map) do
+ if self == expression then
+ return replacement
+ end
+ end
+ return self
+end
+
+--- @return boolean
+function AtomicExpression:isatomic()
+ return true
+end
+
+--- @return string
+function AtomicExpression:tolatex()
+ -- Most atomic expressions should have the same __tostring as LaTeX's output
+ return tostring(self)
+end
+
+
+-----------------
+-- Inheritance --
+-----------------
+
+__AtomicExpression.__index = Expression
+AtomicExpression = setmetatable(AtomicExpression, __AtomicExpression) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation.lua
new file mode 100644
index 0000000000..708a6c1540
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/binaryoperation.lua
@@ -0,0 +1,800 @@
+--- @class BinaryOperation
+--- Represents a binary operation with two inputs and one output.
+--- Represents a generic function that takes zero or more expressions as inputs.
+--- @field name string
+--- @field operation function
+--- @field expressions table<number, Expression>
+BinaryOperation = {}
+__BinaryOperation = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new binary operation with the given operation.
+--- @param operation function
+--- @param expressions table<number, Expression>
+--- @return BinaryOperation
+function BinaryOperation:new(operation, expressions)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ if type(operation) ~= "function" then
+ error("Sent parameter of wrong type: operation must be a function")
+ end
+
+ if type(expressions) ~= "table" then
+ error("Sent parameter of wrong type: expressions must be an array")
+ end
+
+ o.name = BinaryOperation.DEFAULT_NAMES[operation]
+ o.operation = operation
+ o.expressions = Copy(expressions)
+
+ if BinaryOperation.COMMUTATIVITY[operation] then
+ function o:iscommutative()
+ return true
+ end
+ else
+ function o:iscommutative()
+ return false
+ end
+ end
+
+ if not o:iscommutative() and o.operation ~= BinaryOperation.SUB and #o.expressions ~= 2 then
+ error("Sent parameter of wrong type: noncommutative operations cannot have an arbitrary number of paramaters")
+ end
+
+ __o.__index = BinaryOperation
+ __o.__tostring = function(a)
+ local expressionnames = ''
+ for index, expression in ipairs(a.expressions) do
+ if index == 1 and not a.expressions[index + 1] then
+ expressionnames = expressionnames .. a.name .. ' '
+ end
+ if index > 1 then
+ expressionnames = expressionnames .. ' '
+ end
+ if expression:isatomic() and not (a.operation == BinaryOperation.POW and expression:type() == Rational) then
+ expressionnames = expressionnames .. tostring(expression)
+ else
+ expressionnames = expressionnames .. '(' .. tostring(expression) .. ')'
+ end
+ if a.expressions[index + 1] then
+ expressionnames = expressionnames .. ' ' .. a.name
+ end
+ end
+ return expressionnames
+ end
+ __o.__eq = function(a, b)
+ -- This shouldn't be needed, since __eq should only fire if both metamethods have the same function, but for some reason Lua always runs this anyway
+ if not a.operation or not b.operation then
+ return false
+ end
+ local loc = 1
+ while a.expressions[loc] or b.expressions[loc] do
+ if not a.expressions[loc] or not b.expressions[loc] or
+ (a.expressions[loc] ~= b.expressions[loc]) then
+ return false
+ end
+ loc = loc + 1
+ end
+ return a.operation == b.operation
+ end
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- @return Expression
+function BinaryOperation:evaluate()
+ local results = {}
+ local reducible = true
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:evaluate()
+ if not results[index]:isconstant() then
+ reducible = false
+ end
+ end
+ if not reducible then
+ return BinaryOperation(self.operation, results)
+ end
+
+ if not self.expressions[1] then
+ error("Execution error: cannot perform binary operation on zero expressions")
+ end
+
+ local result = results[1]
+ for index, expression in ipairs(results) do
+ if not (index == 1) then
+ result = self.operation(result, expression)
+ end
+ end
+ return result
+end
+
+--- @return Expression
+function BinaryOperation:autosimplify()
+ local results = {}
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:autosimplify()
+ end
+ local simplified = BinaryOperation(self.operation, results)
+ if simplified.operation == BinaryOperation.POW then
+ return simplified:simplifypower()
+ end
+ if simplified.operation == BinaryOperation.MUL then
+ return simplified:simplifyproduct()
+ end
+ if simplified.operation == BinaryOperation.ADD then
+ return simplified:simplifysum()
+ end
+ if simplified.operation == BinaryOperation.DIV then
+ return simplified:simplifyquotient()
+ end
+ if simplified.operation == BinaryOperation.SUB then
+ return simplified:simplifydifference()
+ end
+ return simplified
+end
+
+--- @return table<number, Expression>
+function BinaryOperation:subexpressions()
+ return self.expressions
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return BinaryOperation
+function BinaryOperation:setsubexpressions(subexpressions)
+ return BinaryOperation(self.operation, subexpressions)
+end
+
+--- @return Expression
+function BinaryOperation:expand()
+ local results = {}
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:expand()
+ end
+ local expanded = BinaryOperation(self.operation, results)
+ if expanded.operation == BinaryOperation.MUL then
+ local allsums = BinaryOperation(BinaryOperation.ADD, {Integer.one()})
+ for _, expression in ipairs(expanded.expressions) do
+ allsums = allsums:expand2(expression)
+ end
+ return allsums:autosimplify()
+ end
+ if expanded.operation == BinaryOperation.POW and expanded.expressions[2]:type() == Integer then
+ if expanded.expressions[1]:type() ~= BinaryOperation then
+ return expanded:autosimplify()
+ end
+ local exp = BinaryOperation.MULEXP({Integer.one()})
+ local pow = expanded.expressions[2]:asnumber()
+ for _ = 1, math.abs(pow) do
+ exp = exp:expand2(expanded.expressions[1])
+ if _ > 1 then
+ exp = exp:autosimplify()
+ end
+ end
+ if pow < 0 then
+ exp = exp^Integer(-1)
+ end
+ return exp
+ end
+ if expanded.operation == BinaryOperation.POW and expanded.expressions[2].operation == BinaryOperation.ADD then
+ local exp = {}
+ for i = 1, #expanded.expressions[2].expressions do
+ exp[#exp+1] = (expanded.expressions[1]^expanded.expressions[2].expressions[i]):autosimplify()
+ end
+ return BinaryOperation.MULEXP(exp)
+ end
+ return expanded:autosimplify()
+end
+
+--- Helper for expand - multiplies two addition expressions.
+--- @return Expression
+function BinaryOperation:expand2(other)
+ local result = {}
+ for _, expression in ipairs(self:subexpressions()) do
+ if other:type() == BinaryOperation and other.operation == BinaryOperation.ADD then
+ for _, expression2 in ipairs(other.expressions) do
+ result[#result+1] = expression * expression2
+ end
+ else
+ result[#result+1] = expression * other
+ end
+ end
+ return BinaryOperation(BinaryOperation.ADD, result)
+end
+
+--- @return Expression
+function BinaryOperation:factor()
+ local results = {}
+
+ -- Recursively factors sub-expressions
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:factor()
+ end
+
+ -- Attempts to factor expressions as monovariate polynomials
+ local factoredsubs = BinaryOperation(self.operation, results)
+ local subs = factoredsubs:getsubexpressionsrec()
+ for index, sub in ipairs(subs) do
+ local substituted = factoredsubs:substitute({[sub]=SymbolExpression("_")}):autosimplify()
+ local polynomial, result = substituted:topolynomial()
+ if result then
+ local factored = polynomial:factor():autosimplify()
+ if factored ~= substituted then
+ return factored:substitute({[SymbolExpression("_")]=sub})
+ end
+ end
+ end
+
+ -- Pulls common sub-expressions out of sum expressions
+ if self.operation == BinaryOperation.ADD then
+ local gcf
+ for _, expression in ipairs(factoredsubs:subexpressions()) do
+ if expression.operation ~= BinaryOperation.MUL then
+ expression = BinaryOperation.MULEXP({expression})
+ end
+ if not gcf then
+ gcf = expression
+ else
+ local newgcf = Integer.one()
+ for _, gcfterm in ipairs(gcf:subexpressions()) do
+ local gcfpower = Integer.one()
+ if gcfterm:type() == BinaryOperation and gcfterm.operation == BinaryOperation.POW and gcfterm.expressions[2]:type() == Integer then
+ gcfpower = gcfterm.expressions[2]
+ gcfterm = gcfterm.expressions[1]
+ end
+ for _, term in ipairs(expression:subexpressions()) do
+ local power = Integer.one()
+ if term:type() == BinaryOperation and term.operation == BinaryOperation.POW and term.expressions[2]:type() == Integer then
+ power = term.expressions[2]
+ term = term.expressions[1]
+ end
+ if term == gcfterm then
+ newgcf = newgcf * term^Integer.min(power, gcfpower)
+ end
+ end
+ end
+ gcf = newgcf
+ end
+ end
+ if gcf:type() ~= Integer then
+ local out = Integer.zero()
+ for _, expression in ipairs(factoredsubs:subexpressions()) do
+ out = out + expression/gcf
+ end
+ out = gcf*(out:autosimplify():factor())
+ return out:autosimplify()
+ end
+ end
+
+ return factoredsubs
+end
+
+--- @return Expression
+function BinaryOperation:combine()
+ local den, num, aux, mul, input = {}, {}, {}, {}, self:autosimplify():expand()
+ if input.operation ~= BinaryOperation.ADD then
+ return input
+ end
+ for _, expr in ipairs(input.expressions) do
+ local numpart, denpart = Integer.one(), Integer.one()
+ if expr.operation == BinaryOperation.POW and expr.expressions[2]:type() == Integer and expr.expressions[2] < Integer.zero() then
+ denpart = denpart*expr.expressions[1] ^ expr.expressions[2]:neg()
+ for index,term in ipairs(den) do
+ if expr.expressions[1] == den[index] then
+ if expr.expressions[2]:neg() > mul[index] then
+ mul[index] = expr.expressions[2]:neg()
+ goto continue
+ else
+ goto continue
+ end
+ end
+ end
+ table.insert(den,expr.expressions[1])
+ table.insert(mul,expr.expressions[2]:neg())
+ ::continue::
+ end
+ if expr.operation == BinaryOperation.MUL then
+ for _,subexpr in ipairs(expr.expressions) do
+ if subexpr.operation == BinaryOperation.POW and subexpr.expressions[2]:type() == Integer and subexpr.expressions[2] < Integer.zero() then
+ denpart = denpart*subexpr.expressions[1] ^ subexpr.expressions[2]:neg()
+ for index,term in ipairs(den) do
+ if subexpr.expressions[1] == den[index] then
+ if subexpr.expressions[2]:neg() > mul[index] then
+ mul[index] = subexpr.expressions[2]:neg()
+ goto continue
+ else
+ goto continue
+ end
+ end
+ end
+ table.insert(den,subexpr.expressions[1])
+ table.insert(mul,subexpr.expressions[2]:neg())
+ ::continue::
+ else
+ numpart = numpart*subexpr
+ end
+ end
+ end
+ if expr.operation ~= BinaryOperation.POW and expr.operation ~= BinaryOperation.MUL then
+ numpart = expr
+ end
+ table.insert(num,numpart)
+ table.insert(aux,denpart)
+ end
+ local denominator = Integer.one()
+ local numerator = Integer.zero()
+ for index,expr in ipairs(den) do
+ denominator = denominator*den[index] ^ mul[index]
+ end
+ denominator = denominator:autosimplify()
+ for index,expr in ipairs(num) do
+ local uncommon = denominator/aux[index]
+ uncommon = uncommon:factor():simplify()
+ numerator = numerator + expr*uncommon
+ end
+ numerator = numerator:simplify():factor()
+ if denominator == Integer.one() then
+ return numerator
+ else
+ return numerator/denominator
+ end
+end
+
+--- @param collect Expression
+--- @return Expression
+function BinaryOperation:collect(collect)
+ -- Constant expressions cannot be collected
+ if collect:isconstant() then
+ return self
+ end
+
+ -- Recusively collect subexpressions
+ local results = {}
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:collect(collect)
+ end
+ local collected = BinaryOperation(self.operation, results)
+
+ if not (collected.operation == BinaryOperation.ADD) then
+ return collected:autosimplify()
+ end
+
+ local coefficients = {}
+
+ -- TODO: Add an expression map class
+ setmetatable(coefficients, {__index =
+ function(table, key)
+ local out = rawget(table, tostring(key))
+ return out or Integer.zero()
+ end,
+ __newindex =
+ function (table, key, value)
+ rawset(table, tostring(key), value)
+ end
+ })
+
+ -- Finds all instances of a constant power of the expression to be collected, and maps each power to all terms it is multiplied by
+ for _, expression in ipairs(collected:subexpressions()) do
+ if expression == collect then
+ coefficients[Integer.one()] = coefficients[Integer.one()] + Integer.one()
+ elseif expression.operation == BinaryOperation.POW and expression:subexpressions()[1] == collect and expression:subexpressions()[2]:isconstant() then
+ coefficients[expression:subexpressions()[2]] = coefficients[expression:subexpressions()[2]] + Integer.one()
+ elseif collect:type() == BinaryOperation and collect.operation == BinaryOperation.POW and
+ expression.operation == BinaryOperation.POW and expression:subexpressions()[1] == collect:subexpressions()[1] then
+ -- Handle the fact that autosimplify turns (a^x^n -> a^(xn)), this is needed if the term to collect is itself an exponential
+ local power = (expression:subexpressions()[2] / collect:subexpressions()[2]):autosimplify()
+ if power:isconstant() then
+ coefficients[power] = coefficients[power] + Integer.one()
+ else
+ coefficients[Integer.zero()] = coefficients[Integer.zero()] + expression
+ end
+ elseif expression.operation == BinaryOperation.MUL then
+ local varpart
+ local coeffpart = Integer.one()
+ for _, term in ipairs(expression:subexpressions()) do
+ if term == collect then
+ varpart = Integer.one()
+ elseif (term.operation == BinaryOperation.POW and term:subexpressions()[1] == collect and term:subexpressions()[2]:isconstant()) then
+ varpart = term:subexpressions()[2]
+ elseif collect:type() == BinaryOperation and collect.operation == BinaryOperation.POW and
+ term.operation == BinaryOperation.POW and term:subexpressions()[1] == collect:subexpressions()[1] then
+ local power = (term:subexpressions()[2] / collect:subexpressions()[2]):autosimplify()
+ if power:isconstant() then
+ varpart = power
+ end
+ else
+ coeffpart = coeffpart * term
+ end
+ end
+ if varpart then
+ coefficients[varpart] = coefficients[varpart] + coeffpart
+ else
+ coefficients[Integer.zero()] = coefficients[Integer.zero()] + expression
+ end
+ else
+ coefficients[Integer.zero()] = coefficients[Integer.zero()] + expression
+ end
+
+
+ end
+
+ local out = Integer.zero()
+ for index, value in pairs(coefficients) do
+ out = out + collect ^ Rational.fromstring(index) * value
+ end
+
+ return out:autosimplify()
+end
+
+--- @param other Expression
+--- @return boolean
+function BinaryOperation:order(other)
+ if other:isconstant() then
+ return false
+ end
+
+ if other:isatomic() then
+ if self.operation == BinaryOperation.POW then
+ return self:order(BinaryOperation(BinaryOperation.POW, {other, Integer.one()}))
+ end
+
+ if self.operation == BinaryOperation.MUL then
+ return self:order(BinaryOperation(BinaryOperation.MUL, {other}))
+ end
+
+ if self.operation == BinaryOperation.ADD then
+ return self:order(BinaryOperation(BinaryOperation.ADD, {other}))
+ end
+ end
+
+ if self.operation == BinaryOperation.POW and other.operation == BinaryOperation.POW then
+ if self.expressions[1] ~= other.expressions[1] then
+ return self.expressions[1]:order(other.expressions[1])
+ end
+ return self.expressions[2]:order(other.expressions[2])
+ end
+
+ if (self.operation == BinaryOperation.MUL and other.operation == BinaryOperation.MUL) or
+ (self.operation == BinaryOperation.ADD and other.operation == BinaryOperation.ADD) then
+ local k = 0
+ while #self.expressions - k > 0 and #other.expressions - k > 0 do
+ if self.expressions[#self.expressions - k] ~= other.expressions[#other.expressions - k] then
+ return self.expressions[#self.expressions - k]:order(other.expressions[#other.expressions - k])
+ end
+ k = k + 1
+ end
+ return #self.expressions < #other.expressions
+ end
+
+ if (self.operation == BinaryOperation.MUL) and (other.operation == BinaryOperation.POW or other.operation == BinaryOperation.ADD) then
+ return self:order(BinaryOperation(BinaryOperation.MUL, {other}))
+ end
+
+ if (self.operation == BinaryOperation.POW) and (other.operation == BinaryOperation.MUL) then
+ return BinaryOperation(BinaryOperation.MUL, {self}):order(other)
+ end
+
+ if (self.operation == BinaryOperation.POW) and (other.operation == BinaryOperation.ADD) then
+ return self:order(BinaryOperation(BinaryOperation.POW, {other, Integer.one()}))
+ end
+
+ if (self.operation == BinaryOperation.ADD) and (other.operation == BinaryOperation.MUL) then
+ return BinaryOperation(BinaryOperation.MUL, {self}):order(other)
+ end
+
+ if (self.operation == BinaryOperation.ADD) and (other.operation == BinaryOperation.POW) then
+ return BinaryOperation(BinaryOperation.POW, {self, Integer.one()}):order(other)
+ end
+
+ if other:type() == FunctionExpression or other:type() == TrigExpression or other:type() == Logarithm then
+ if self.operation == BinaryOperation.ADD or self.operation == BinaryOperation.MUL then
+ return self:order(BinaryOperation(self.operation, {other}))
+ end
+
+ if self.operation == BinaryOperation.POW then
+ return self:order(other^Integer.one())
+ end
+ end
+
+ return true
+end
+
+--- Returns whether the binary operation is commutative.
+--- @return boolean
+function BinaryOperation:iscommutative()
+ error("Called unimplemented method: iscommutative()")
+end
+
+--- @return PolynomialRing, boolean
+function BinaryOperation:topolynomial()
+ local addexp = self
+ if not self.operation or self.operation ~= BinaryOperation.ADD then
+ addexp = BinaryOperation(BinaryOperation.ADD, {self})
+ end
+
+ local poly = {}
+ local degree = 0
+ local symbol
+ for _, expression in ipairs(addexp.expressions) do
+ local coefficient
+ local sym
+ local power
+ -- Expressions of the form c
+ if expression:isconstant() then
+ coefficient = expression
+ power = 0
+ -- Expressions of the form x
+ elseif expression:type() == SymbolExpression then
+ coefficient = Integer.one()
+ sym = expression.symbol
+ power = 1
+ -- Expressions of the form c*x
+ elseif expression.operation and expression.operation == BinaryOperation.MUL and #expression.expressions == 2
+ and expression.expressions[1]:isconstant() and expression.expressions[2]:type() == SymbolExpression then
+
+ coefficient = expression.expressions[1]
+ sym = expression.expressions[2].symbol
+ power = 1
+ -- Expressions of the form c*x^n (totally not confusing)
+ elseif expression.operation and expression.operation == BinaryOperation.MUL and #expression.expressions == 2
+ and expression.expressions[1]:isconstant() and expression.expressions[2].operation and
+ expression.expressions[2].operation == BinaryOperation.POW and #expression.expressions[2].expressions == 2
+ and expression.expressions[2].expressions[1]:type() == SymbolExpression and expression.expressions[2].expressions[2].getring
+ and expression.expressions[2].expressions[2]:getring() == Integer.getring() and expression.expressions[2].expressions[2] > Integer.zero() then
+
+ coefficient = expression.expressions[1]
+ sym = expression.expressions[2].expressions[1].symbol
+ power = expression.expressions[2].expressions[2]:asnumber()
+ -- Expressions of the form x^n
+ elseif expression.operation and expression.operation == BinaryOperation.POW and #expression.expressions == 2
+ and expression.expressions[1]:type() == SymbolExpression and expression.expressions[2].getring
+ and expression.expressions[2]:getring() == Integer.getring() and expression.expressions[2] > Integer.zero() then
+
+ coefficient = Integer.one()
+ sym = expression.expressions[1].symbol
+ power = expression.expressions[2]:asnumber()
+ else
+ return self, false
+ end
+
+ if symbol and sym and symbol ~= sym then
+ return self, false
+ end
+ if not symbol then
+ symbol = sym
+ end
+ poly[power + 1] = coefficient
+ if power > degree then
+ degree = power
+ end
+ end
+
+ for i = 1,degree+1 do
+ poly[i] = poly[i] or Integer.zero()
+ end
+
+ return PolynomialRing(poly, symbol), true
+end
+
+function BinaryOperation:tolatex()
+ if self.operation == BinaryOperation.POW then
+ if self.expressions[2]:type() == Integer and self.expressions[2] < Integer.zero() then
+ local base = self.expressions[1]
+ local exponent = self.expressions[2]
+ if exponent == Integer(-1) then
+ return "\\frac{1}{" .. base:tolatex() .. "}"
+ else
+ if base:isatomic() then
+ return "\\frac{1}{" .. base:tolatex() .. "^{" .. exponent:neg():tolatex() .. "}}"
+ else
+ return "\\frac{1}{\\left(" .. base:tolatex() .. "\\right)^{" .. exponent:neg():tolatex() .. "}}"
+ end
+ end
+ end
+ if self.expressions[1]:isatomic() then
+ if self.expressions[2]:isconstant() and self.expressions[2]:getring() == Rational:getring() and self.expressions[2].numerator == Integer.one() then
+ if self.expressions[2].denominator == Integer(2) then
+ return "\\sqrt{" .. self.expressions[1]:tolatex() .. '}'
+ end
+ return "\\sqrt[" .. self.expressions[2].denominator:tolatex() .. ']{' .. self.expressions[1]:tolatex() .. '}'
+ end
+ return self.expressions[1]:tolatex() .. '^{' .. self.expressions[2]:tolatex() .. '}'
+ else
+ if self.expressions[2]:isconstant() and self.expressions[2]:getring() == Rational:getring() and self.expressions[2].numerator == Integer.one() then
+ if self.expressions[2].denominator == Integer(2) then
+ return "\\sqrt{" .. self.expressions[1]:tolatex() .. '}'
+ end
+ return "\\sqrt[" .. self.expressions[2].denominator:tolatex() .. ']{' .. self.expressions[1]:tolatex() .. '}'
+ end
+ return "\\left(" .. self.expressions[1]:tolatex() .. "\\right)" .. '^{' .. self.expressions[2]:tolatex() .. '}'
+ end
+ end
+ if self.operation == BinaryOperation.MUL then
+ local sign = ''
+ local out = ''
+ local denom = ''
+ if self:autosimplify():isconstant() then
+ for index, expression in ipairs(self.expressions) do
+ if index == 1 then
+ out = out .. expression:tolatex()
+ else
+ out = out .. "\\cdot " .. expression:tolatex()
+ end
+ end
+ return out
+ end
+ if #self.expressions == 2 and self.expressions[2]:type() == BinaryOperation and self.expressions[2].operation == BinaryOperation.POW and self.expressions[2].expressions[2] == -Integer.one() then
+ out = '\\frac{' .. self.expressions[1]:tolatex() .. '}{' .. self.expressions[2].expressions[1]:tolatex() .. '}'
+ return out
+ end
+ for _, expression in ipairs(self.expressions) do
+ if expression:type() == BinaryOperation then
+ if expression.operation == BinaryOperation.POW and expression.expressions[2]:isconstant() and expression.expressions[2] < Integer.zero() then
+ local reversed = (Integer.one() / expression):autosimplify()
+ if reversed.operation == BinaryOperation.ADD or expression.operation == BinaryOperation.SUB then
+ denom = denom .. '\\left('.. reversed:tolatex() .. '\\right)'
+ else
+ denom = denom .. reversed:tolatex()
+ end
+ elseif expression.operation == BinaryOperation.ADD or expression.operation == BinaryOperation.SUB then
+ out = out .. '\\left(' .. expression:tolatex() .. '\\right)'
+ else
+ out = out .. expression:tolatex()
+ end
+ else
+ if expression == Integer(-1) then
+ out = out .. '-'
+ elseif expression:type() == Rational and expression.numerator == Integer.one() then
+ denom = denom .. expression.denominator:tolatex()
+ elseif expression:type() == Rational and expression.numerator == Integer(-1) then
+ out = out .. '-'
+ denom = denom .. expression.denominator:tolatex()
+ elseif expression:type() == Rational then
+ out = out .. expression.numerator:tolatex()
+ denom = denom .. expression.denominator:tolatex()
+ else
+ out = out .. expression:tolatex()
+ end
+ end
+ end
+ if string.sub(out,1,1) == '-' then
+ sign = '-'
+ out = string.sub(out,2,-1)
+ end
+ if denom ~= '' and out == '' then
+ return sign .. '\\frac{' .. '1' .. '}{' .. denom .. '}'
+ end
+ if denom ~= '' then
+ return sign .. '\\frac{' .. out .. '}{' .. denom .. '}'
+ end
+ return sign..out
+ end
+ if self.operation == BinaryOperation.ADD then
+ local out = ''
+ for index, expression in ipairs(self.expressions) do
+ out = out .. expression:tolatex()
+ if self.expressions[index + 1] and string.sub(self.expressions[index + 1]:tolatex(), 1, 1) ~= "-" then
+ out = out .. '+'
+ end
+ end
+ return out
+ end
+ if self.operation == BinaryOperation.DIV then
+ return '\\frac{' .. self.expressions[1]:tolatex() .. '}{' .. self.expressions[2]:tolatex() .. '}'
+ end
+ if self.operation == BinaryOperation.SUB then
+ local out = ''
+ if not self.expressions[2] then
+ if not self.expressions[1]:isatomic() then
+ out = '-\\left(' .. self.expressions[1]:tolatex() .. '\\right)'
+ else
+ out = '-' .. self.expressions[1]:tolatex()
+ end
+ else
+ for index, expression in ipairs(self.expressions) do
+ if expression.operation and (expression.operation == BinaryOperation.ADD or expression.operation == BinaryOperation.SUB) and index >1 then
+ out = out .. "\\left(" .. expression:tolatex() .. "\\right)"
+ else
+ out = out .. expression:tolatex()
+ end
+ if self.expressions[index + 1] then
+ out = out .. '-'
+ end
+ end
+ end
+ return out
+ end
+ return self
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__BinaryOperation.__index = CompoundExpression
+__BinaryOperation.__call = BinaryOperation.new
+BinaryOperation = setmetatable(BinaryOperation, __BinaryOperation)
+
+----------------------
+-- Static constants --
+----------------------
+
+BinaryOperation.ADD = function(a, b)
+ return a + b
+end
+
+BinaryOperation.SUB = function(a, b)
+ return a - b
+end
+
+BinaryOperation.MUL = function(a, b)
+ return a * b
+end
+
+BinaryOperation.DIV = function(a, b)
+ return a / b
+end
+
+BinaryOperation.IDIV = function(a, b)
+ return a // b
+end
+
+BinaryOperation.MOD = function(a, b)
+ return a % b
+end
+
+BinaryOperation.POW = function(a, b)
+ return a ^ b
+end
+
+BinaryOperation.DEFAULT_NAMES = {
+ [BinaryOperation.ADD] = "+",
+ [BinaryOperation.SUB] = "-",
+ [BinaryOperation.MUL] = "*",
+ [BinaryOperation.DIV] = "/",
+ [BinaryOperation.IDIV] = "//",
+ [BinaryOperation.MOD] = "%",
+ [BinaryOperation.POW] = "^"
+}
+
+BinaryOperation.COMMUTATIVITY = {
+ [BinaryOperation.ADD] = true,
+ [BinaryOperation.SUB] = false,
+ [BinaryOperation.MUL] = true,
+ [BinaryOperation.DIV] = false,
+ [BinaryOperation.IDIV] = false,
+ [BinaryOperation.MOD] = false,
+ [BinaryOperation.POW] = false
+}
+
+BinaryOperation.ADDEXP = function(expressions, name)
+ return BinaryOperation(BinaryOperation.ADD, expressions, name)
+end
+
+BinaryOperation.SUBEXP = function(expressions, name)
+ return BinaryOperation(BinaryOperation.SUB, expressions, name)
+end
+
+BinaryOperation.MULEXP = function(expressions, name)
+ return BinaryOperation(BinaryOperation.MUL, expressions, name)
+end
+
+BinaryOperation.DIVEXP = function(expressions, name)
+ return BinaryOperation(BinaryOperation.DIV, expressions, name)
+end
+
+BinaryOperation.IDIVEXP = function(expressions, name)
+ return BinaryOperation(BinaryOperation.IDIV, expressions, name)
+end
+
+BinaryOperation.MODEXP = function(expressions, name)
+ return BinaryOperation(BinaryOperation.MOD, expressions, name)
+end
+
+BinaryOperation.POWEXP = function(expressions, name)
+ return BinaryOperation(BinaryOperation.POW, expressions, name)
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/difference.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/difference.lua
new file mode 100644
index 0000000000..2cc56b6600
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/difference.lua
@@ -0,0 +1,14 @@
+-- Seperates the various binary operations into their own files for readability
+
+--- Automatic simplification of difference expressions.
+--- @return BinaryOperation
+function BinaryOperation:simplifydifference()
+ local term1 = self.expressions[1]
+ local term2 = self.expressions[2]
+
+ if not term2 then
+ return BinaryOperation(BinaryOperation.MUL, {Integer(-1), term1}):autosimplify()
+ end
+
+ return BinaryOperation(BinaryOperation.ADD, {term1, BinaryOperation(BinaryOperation.MUL, {Integer(-1), term2}):autosimplify()}):autosimplify()
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/power.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/power.lua
new file mode 100644
index 0000000000..35df72c440
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/power.lua
@@ -0,0 +1,169 @@
+-- Seperates the various binary operations into their own files for readability
+
+--- Automatic simplification of power expressions.
+--- @return BinaryOperation
+function BinaryOperation:simplifypower()
+ local base = self.expressions[1]
+ local exponent = self.expressions[2]
+
+ if base:isconstant() and exponent:isconstant() and exponent:getring() ~= Rational:getring() then
+ return self:evaluate()
+ end
+
+ -- Simplifies i^x for x integer.
+ if base == I and exponent:isconstant() and exponent:getring() == Integer:getring() then
+ if exponent % Integer(4) == Integer(0) then
+ return Integer(1)
+ end
+ if exponent % Integer(4) == Integer(1) then
+ return I
+ end
+ if exponent % Integer(4) == Integer(2) then
+ return Integer(-1)
+ end
+ if exponent % Integer(4) == Integer(3) then
+ return -I
+ end
+ end
+
+ -- Simplifies complex numbers raised to negative integer powers
+ if not base:isrealconstant() and base:iscomplexconstant() and exponent:isconstant() and exponent:getring() == Integer:getring() and exponent < Integer.zero() then
+ local a
+ local b
+ if base.operation == BinaryOperation.MUL then
+ a = Integer.zero()
+ b = base.expressions[1]
+ elseif base.operation == BinaryOperation.ADD and base.expressions[2] == I then
+ a = base.expressions[1]
+ b = Integer.one()
+ else
+ a = base.expressions[1]
+ b = base.expressions[2].expressions[1]
+ end
+ return (((a-b*I)/(a^Integer(2)+b^Integer(2)))^(-exponent)):expand():autosimplify()
+ end
+
+ -- Uses the property that 0^x = 0 if x does not equal 0
+ if base:isconstant() and base == base:zero() then
+ return Integer.zero()
+ end
+
+ -- Uses the property that 1^x = 1
+ if base:isconstant() and base == base:one() then
+ return base:one()
+ end
+
+ -- Uses the property that x^0 = 1
+ if exponent:isconstant() and exponent == exponent:zero() then
+ return exponent:one()
+ end
+
+ -- Uses the property that x^1 = x
+ if exponent:isconstant() and exponent == exponent:one() then
+ return base
+ end
+
+ -- Uses the property that b ^ (log(b, x)) == x
+ if exponent:type() == Logarithm and exponent.base == base then
+ return exponent.expression
+ end
+
+ -- Uses the property that b ^ (a * log(b, x)) == x ^ a
+ if exponent.operation == BinaryOperation.MUL then
+ local x
+ local rest = Integer.one()
+ for _, expression in ipairs(exponent.expressions) do
+ if expression:type() == Logarithm and expression.base == base and not log then
+ x = expression.expression
+ else
+ rest = rest * expression
+ end
+ end
+ if x then
+ return (x ^ rest):autosimplify()
+ end
+ end
+
+ -- Uses the property that (x^a)^b = x^(a*b)
+ if not base:isatomic() and base.operation == BinaryOperation.POW and exponent:isconstant() then
+ base, exponent = base.expressions[1], BinaryOperation(BinaryOperation.MUL, {exponent, base.expressions[2]}):autosimplify()
+ return BinaryOperation(BinaryOperation.POW, {base, exponent}):autosimplify()
+ end
+
+ -- Uses the property that (x_1*x_2*...*x_n)^a = x_1^a*x_2^a*..x_n^a if a is an integer
+ if base.operation == BinaryOperation.MUL and exponent:type() == Integer then
+ local results = {}
+ for index, expression in ipairs(base.expressions) do
+ results[index] = BinaryOperation(BinaryOperation.POW, {expression, exponent}):autosimplify()
+ end
+ return BinaryOperation(BinaryOperation.MUL, results):autosimplify()
+ end
+
+ -- Uses the property that sqrt(x,r)^d == sqrt(x,r/d)
+ if base:type() == SqrtExpression and exponent:type() == Integer and exponent > Integer.zero() then
+ local root = base.root
+ local expr = base.expression
+ local comm = Integer.gcd(root,exponent)
+ root = root / comm
+ local expo = exponent / comm
+ expr = expr ^ expo
+ return SqrtExpression(expr,root):autosimplify()
+ end
+
+ -- Rationalizing SqrtExpressions
+ if base:type() == SqrtExpression and exponent:type() == Integer and base.expression:type() == Integer and exponent < Integer.zero() then
+ local root = base.root
+ local expr = base.expression
+ local result = (SqrtExpression(expr ^ (root - Integer.one()),root) / expr) ^ exponent:neg()
+ return result:autosimplify()
+ end
+
+ if base:isconstant() and exponent:isconstant() and exponent:getring() == Rational.getring() then
+ return self --:simplifyrationalpower()
+ end
+
+ -- Our expression cannot be simplified
+ return self
+end
+
+-- Automatic simplification of rational power expressions
+function BinaryOperation:simplifyrationalpower()
+ local base = self.expressions[1]
+ local exponent = self.expressions[2]
+
+ if base:getring() == Rational.getring() then
+ return (BinaryOperation(BinaryOperation.POW, {base.numerator, exponent}):simplifyrationalpower()) /
+ (BinaryOperation(BinaryOperation.POW, {base.denominator, exponent}):simplifyrationalpower())
+ end
+
+ if base == Integer(-1) then
+ if exponent == Integer(1) / Integer(2) then
+ return I
+ end
+
+ return self
+ end
+
+ local primes = base:primefactorization()
+
+ if primes.expressions[1] and not primes.expressions[2] then
+ local primeexponent = primes.expressions[1].expressions[2]
+ local primebase = primes.expressions[1].expressions[1]
+ local newexponent = primeexponent * exponent
+ local integerpart
+ if newexponent.getring() == Rational.getring() then
+ integerpart = newexponent.numerator // newexponent.denominator
+ else
+ integerpart = newexponent
+ end
+
+ if integerpart == Integer.zero() then
+ return BinaryOperation(BinaryOperation.POW, {primebase, newexponent})
+ end
+ return BinaryOperation(BinaryOperation.MUL,
+ {BinaryOperation(BinaryOperation.POW, {primebase, integerpart}),
+ BinaryOperation(BinaryOperation.POW, {primebase, newexponent - integerpart})}):autosimplify()
+ end
+
+ return BinaryOperation(BinaryOperation.POW, {primes:autosimplify(), exponent})
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/product.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/product.lua
new file mode 100644
index 0000000000..b104cb5e8a
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/product.lua
@@ -0,0 +1,231 @@
+-- Seperates the various binary operations into their own files for readability
+
+--- Automatic simplification of multiplication expressions.
+--- @return BinaryOperation
+function BinaryOperation:simplifyproduct()
+ if not self.expressions[1] then
+ error("Execution error: attempted to simplify empty product")
+ end
+
+ if not self.expressions[2] then
+ return self.expressions[1]
+ end
+
+ -- Uses the property that x*0=0
+ for _, expression in ipairs(self.expressions) do
+ if expression:isconstant() and expression == expression:zero() then
+ return expression:zero()
+ end
+ end
+
+ local result = self:simplifyproductrec()
+
+ -- We don't really know what ring we are working in here, so just assume the integer ring
+ if not result.expressions[1] then
+ return Integer.one()
+ end
+
+ if not result.expressions[2] then
+ return result.expressions[1]
+ end
+
+ return result
+end
+
+function BinaryOperation:simplifyproductrec()
+ local term1 = self.expressions[1]
+ local term2 = self.expressions[2]
+
+ if not self.expressions[3] then
+ if (term1:isconstant() or not (term1.operation == BinaryOperation.MUL)) and
+ (term2:isconstant() or not (term2.operation == BinaryOperation.MUL)) then
+
+ if term1:isconstant() and term2:isconstant() then
+ local result = self:evaluate()
+ if result == result:one() then
+ return BinaryOperation(BinaryOperation.MUL, {})
+ end
+ return BinaryOperation(BinaryOperation.MUL, {result})
+ end
+
+ -- Uses the property that x*1 = x
+ if term1:isconstant() and term1 == term1:one() then
+ return BinaryOperation(BinaryOperation.MUL, {term2})
+ end
+
+ if term2:isconstant() and term2 == term2:one() then
+ return BinaryOperation(BinaryOperation.MUL, {term1})
+ end
+
+ -- Distributes constants if the other term is a sum expression.
+ if term1:isconstant() and term2.operation == BinaryOperation.ADD then
+ local distributed = BinaryOperation(BinaryOperation.ADD, {})
+ for i, exp in ipairs(term2:subexpressions()) do
+ distributed.expressions[i] = term1 * exp
+ end
+ return BinaryOperation(BinaryOperation.MUL, {distributed:autosimplify()})
+ end
+
+ if term2:isconstant() and term1.operation == BinaryOperation.ADD then
+ local distributed = BinaryOperation(BinaryOperation.ADD, {})
+ for i, exp in ipairs(term1:subexpressions()) do
+ distributed.expressions[i] = term2 * exp
+ end
+ return BinaryOperation(BinaryOperation.MUL, {distributed:autosimplify()})
+ end
+
+ -- Uses the property that sqrt(a,r)*sqrt(b,r) = sqrt(a*b,r) if a,r are positive integers
+ if term1:type() == SqrtExpression and term2:type() == SqrtExpression and term1.expression:isconstant() and term2.expression:isconstant() and term1.root:type() == Integer then
+ if term1.root == term2.root and term1.expression > Integer.zero() then
+ local expression = term1.expression*term2.expression
+ local result = SqrtExpression(expression,term1.root):autosimplify()
+ if result == Integer.one() then
+ return BinaryOperation(BinaryOperation.MUL,{})
+ else
+ return BinaryOperation(BinaryOperation.MUL,{result})
+ end
+ end
+ end
+
+ --if term1.operation == BinaryOperation.POW and term2.operation == BinaryOperation.POW and term1.expressions[1]:type() == SqrtExpression and term2.expressions[1]:type() == SqrtExpression and term1.expressions[2]:type() == Integer and term2.expressions[2]:type() == Integer and term1.expressions[2] < Integer.zero() and term2.expressions[2] < Integer.zero() and term1.expressions[1].root == term2.expressions[1].root then
+ -- local expo1 = term1.expressions[2]:neg()
+ -- local expo2 = term2.expressions[2]:neg()
+ -- local root = term1.expressions[1].root
+ -- local expr1 = term1.expressions[1].expression
+ -- local expr2 = term2.expressions[1].expression
+ -- local result1 = BinaryOperation(BinaryOperation.POW,{SqrtExpression(expr1,root),expo1}):simplifypower()
+ -- local result2 = BinaryOperation(BinaryOperation.POW,{SqrtExpression(expr2,root),expo2}):simplifypower()
+ -- local result = BinaryOperation(BinaryOperation.MUL,{result1,result2}):autosimplify()
+ -- if result == Integer.one() then
+ -- return BinaryOperation(BinaryOperation.MUL,{})
+ -- end
+ -- if result:type() == Integer then
+ -- return BinaryOperation(BinaryOperation.MUL,{Rational(Integer.one(),result)})
+ -- end
+ -- return BinaryOperation(BinaryOperation.MUL,{BinaryOperation(BinaryOperation.POW, {result,Integer(-1)})}):autosimplify()
+ --end
+
+ -- Uses the property that x^a*x^b=x^(a+b)
+ local revertterm1 = false
+ local revertterm2 = false
+ if term1.operation ~= BinaryOperation.POW then
+ term1 = BinaryOperation(BinaryOperation.POW, {term1, Integer.one()})
+ revertterm1 = true
+ end
+ if term2.operation ~= BinaryOperation.POW then
+ term2 = BinaryOperation(BinaryOperation.POW, {term2, Integer.one()})
+ revertterm2 = true
+ end
+ if term1.expressions[1] == term2.expressions[1]
+ --and not
+ -- (term1.expressions[1]:type() == Integer and
+ -- term1.expressions[2]:type() ~= term2.--expressions[2]:type())
+ then
+ local result = BinaryOperation(BinaryOperation.POW,
+ {term1.expressions[1],
+ BinaryOperation(BinaryOperation.ADD,
+ {term1.expressions[2], term2.expressions[2]}):autosimplify()}):autosimplify()
+ if result:isconstant() and result == result:one() then
+ return BinaryOperation(BinaryOperation.MUL, {})
+ end
+ return BinaryOperation(BinaryOperation.MUL, {result})
+ end
+
+ if revertterm1 then
+ term1 = term1.expressions[1]
+ end
+ if revertterm2 then
+ term2 = term2.expressions[1]
+ end
+
+ if term2:order(term1) then
+ return BinaryOperation(BinaryOperation.MUL, {term2, term1})
+ end
+
+ return self
+ end
+
+ if term1.operation == BinaryOperation.MUL and not (term2.operation == BinaryOperation.MUL) then
+ return term1:mergeproducts(BinaryOperation(BinaryOperation.MUL, {term2}))
+ end
+
+ if not (term1.operation == BinaryOperation.MUL) and term2.operation == BinaryOperation.MUL then
+ return BinaryOperation(BinaryOperation.MUL, {term1}):mergeproducts(term2)
+ end
+
+ return term1:mergeproducts(term2)
+ end
+
+ local rest = {}
+ for index, expression in ipairs(self.expressions) do
+ if index > 1 then
+ rest[index - 1] = expression
+ end
+ end
+
+ local result = BinaryOperation(BinaryOperation.MUL, rest):simplifyproductrec()
+
+ if term1.operation ~= BinaryOperation.MUL then
+ term1 = BinaryOperation(BinaryOperation.MUL, {term1})
+ end
+ if result.operation ~= BinaryOperation.MUL then
+ result = BinaryOperation(BinaryOperation.MUL, {result})
+ end
+ return term1:mergeproducts(result)
+end
+
+-- Merges two lists of products
+function BinaryOperation:mergeproducts(other)
+ if not self.expressions[1] then
+ return other
+ end
+
+ if not other.expressions[1] then
+ return self
+ end
+
+ local first = BinaryOperation(BinaryOperation.MUL, {self.expressions[1], other.expressions[1]}):simplifyproductrec()
+
+ local selfrest = {}
+ for index, expression in ipairs(self.expressions) do
+ if index > 1 then
+ selfrest[index - 1] = expression
+ end
+ end
+
+ local otherrest = {}
+ for index, expression in ipairs(other.expressions) do
+ if index > 1 then
+ otherrest[index - 1] = expression
+ end
+ end
+
+ if first.operation ~= BinaryOperation.MUL or not first.expressions[2] then
+ local result = BinaryOperation(self.operation, selfrest):mergeproducts(BinaryOperation(other.operation, otherrest))
+ if not first.expressions[1] then
+ return result
+ end
+ table.insert(result.expressions, 1, first.expressions[1])
+
+ if result.operation == BinaryOperation.MUL and not result.expressions[3] and result.expressions[1] and result.expressions[2] then
+ if result.expressions[1]:isconstant() and result.expressions[2]:isconstant() then
+ return result:simplifyproductrec()
+ end
+ end
+ return result
+ end
+
+ local result
+ if first.expressions[1] == self.expressions[1] then
+ result = BinaryOperation(self.operation, selfrest):mergeproducts(other)
+ else
+ result = self:mergeproducts(BinaryOperation(other.operation, otherrest))
+ end
+ table.insert(result.expressions, 1, first.expressions[1])
+ if result.operation == BinaryOperation.MUL and not result.expressions[3] and result.expressions[1] and result.expressions[2] then
+ if result.expressions[1]:isconstant() and result.expressions[2]:isconstant() then
+ return result:simplifyproductrec()
+ end
+ end
+ return result
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/quotient.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/quotient.lua
new file mode 100644
index 0000000000..4dac489bf5
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/quotient.lua
@@ -0,0 +1,14 @@
+-- Seperates the various binary operations into their own files for readability
+
+-- Automatic simplification of quotient expressions.
+--- @return BinaryOperation
+function BinaryOperation:simplifyquotient()
+ local numerator = self.expressions[1]
+ local denominator = self.expressions[2]
+
+ if numerator:isconstant() and denominator:isconstant() then
+ return self:evaluate()
+ end
+
+ return BinaryOperation(BinaryOperation.MUL, {numerator, BinaryOperation(BinaryOperation.POW, {denominator, Integer(-1)}):autosimplify()}):autosimplify()
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/binaryoperation/sum.lua b/macros/luatex/latex/luacas/tex/core/binaryoperation/sum.lua
new file mode 100644
index 0000000000..5bb204066f
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/binaryoperation/sum.lua
@@ -0,0 +1,226 @@
+-- Seperates the various binary operations into their own files for readability
+
+-- Automatic simplification of addition expressions.
+--- @return BinaryOperation
+function BinaryOperation:simplifysum()
+ if not self.expressions[1] then
+ error("Execution error: Attempted to simplify empty sum")
+ end
+
+ if not self.expressions[2] then
+ return self.expressions[1]
+ end
+
+ local result = self:simplifysumrec()
+
+ -- We don't really know what ring we are working in here, so just assume the integer ring
+ if not result.expressions[1] then
+ return Integer.zero()
+ end
+
+ -- Simplifies single sums to their operands
+ if not result.expressions[2] then
+ return result.expressions[1]
+ end
+
+ return result
+end
+
+function BinaryOperation:simplifysumrec()
+ local term1 = self.expressions[1]
+ local term2 = self.expressions[2]
+
+ if self.expressions[1] and self.expressions[2] and not self.expressions[3] then
+ if (term1:isconstant() or not (term1.operation == BinaryOperation.ADD)) and
+ (term2:isconstant() or not (term2.operation == BinaryOperation.ADD)) then
+
+ if term1:isconstant() and term2:isconstant() then
+ return BinaryOperation(BinaryOperation.ADD, {self:evaluate()})
+ end
+
+ -- Uses the property that x + 0 = x
+ if term1:isconstant() and term1 == term1:zero() then
+ return BinaryOperation(BinaryOperation.ADD, {term2})
+ end
+
+ if term2:isconstant() and term2 == term2:zero() then
+ return BinaryOperation(BinaryOperation.ADD, {term1})
+ end
+
+ local revertterm1 = false
+ local revertterm2 = false
+ -- Uses the property that a*x+b*x= (a+b)*x
+ -- This is only done if a and b are constant, since otherwise this could be counterproductive
+ -- We SHOULD be okay to only check left distributivity, since constants always come first when ordered
+ if term1.operation == BinaryOperation.MUL and term2.operation == BinaryOperation.MUL then
+ local findex = 2
+ local sindex = 2
+ if not term1.expressions[1]:isconstant() then
+ revertterm1 = true
+ findex = 1
+ end
+ if not term2.expressions[1]:isconstant() then
+ revertterm2 = true
+ sindex = 1
+ end
+ if FancyArrayEqual(term1.expressions,term2.expressions,findex,sindex) then
+ local result
+ if not revertterm1 and not revertterm2 then
+ result = BinaryOperation(
+ BinaryOperation.ADD,
+ {term1.expressions[1],term2.expressions[1]}
+ )
+ end
+ if revertterm1 and not revertterm2 then
+ result = BinaryOperation(
+ BinaryOperation.ADD,
+ {Integer.one(),term2.expressions[1]}
+ )
+ end
+ if not revertterm1 and revertterm2 then
+ result = BinaryOperation(
+ BinaryOperation.ADD,
+ {term1.expressions[1],Integer.one()}
+ )
+ end
+ if revertterm1 and revertterm2 then
+ result = Integer(2)
+ end
+ result = result:autosimplify()
+ for i=findex,#term1.expressions do
+ result = BinaryOperation(
+ BinaryOperation.MUL,
+ {result,term1.expressions[i]}
+ )
+ end
+ result = result:autosimplify()
+ if result:isconstant() and result == result:zero() then
+ return BinaryOperation(BinaryOperation.ADD, {})
+ end
+ return BinaryOperation(BinaryOperation.ADD, {result})
+ end
+ end
+
+ if term1.operation ~= BinaryOperation.MUL or not term1.expressions[1]:isconstant() then
+ term1 = BinaryOperation(BinaryOperation.MUL,{Integer.one(), term1})
+ revertterm1 = true
+ end
+ if term2.operation ~= BinaryOperation.MUL or not term2.expressions[1]:isconstant() then
+ term2 = BinaryOperation(BinaryOperation.MUL, {Integer.one(), term2})
+ revertterm2 = true
+ end
+ if ArrayEqual(term1.expressions, term2.expressions, 2) then
+ local result = BinaryOperation(
+ BinaryOperation.ADD,
+ {term1.expressions[1],term2.expressions[1]}
+ )
+ result = result:autosimplify()
+ for i=2,#term1.expressions do
+ result = BinaryOperation(
+ BinaryOperation.MUL,
+ {result,term1.expressions[i]}
+ )
+ end
+ result = result:autosimplify()
+ --local result = BinaryOperation(BinaryOperation.MUL,
+ -- {BinaryOperation(BinaryOperation.ADD,
+ -- {term1.expressions[1],
+ -- term2.expressions[1]}):autosimplify(),
+ -- term1.expressions[2]}):autosimplify()
+ if result:isconstant() and result == result:zero() then
+ return BinaryOperation(BinaryOperation.ADD, {})
+ end
+ return BinaryOperation(BinaryOperation.ADD, {result})
+ end
+
+ if revertterm1 then
+ term1 = term1.expressions[2]
+ end
+ if revertterm2 then
+ term2 = term2.expressions[2]
+ end
+
+ if term2:order(term1) then
+ return BinaryOperation(BinaryOperation.ADD, {term2, term1})
+ end
+
+ return self
+ end
+
+ if term1.operation == BinaryOperation.ADD and not (term2.operation == BinaryOperation.ADD) then
+ return term1:mergesums(BinaryOperation(BinaryOperation.ADD, {term2}))
+ end
+
+ if not (term1.operation == BinaryOperation.ADD) and term2.operation == BinaryOperation.ADD then
+ return BinaryOperation(BinaryOperation.ADD, {term1}):mergesums(term2)
+ end
+
+ return term1:mergesums(term2)
+ end
+
+ local rest = {}
+ for index, expression in ipairs(self.expressions) do
+ if index > 1 then
+ rest[index - 1] = expression
+ end
+ end
+
+ local result = BinaryOperation(BinaryOperation.ADD, rest):simplifysumrec()
+
+ if term1.operation ~= BinaryOperation.ADD then
+ term1 = BinaryOperation(BinaryOperation.ADD, {term1})
+ end
+ if result.operation ~= BinaryOperation.ADD then
+ result = BinaryOperation(BinaryOperation.ADD, {result})
+ end
+ return term1:mergesums(result)
+end
+
+-- Merges two lists of sums
+function BinaryOperation:mergesums(other)
+ if not self.expressions[1] then
+ return other
+ end
+
+ if not other.expressions[1] then
+ return self
+ end
+
+ local first = BinaryOperation(BinaryOperation.ADD, {self.expressions[1], other.expressions[1]}):simplifysumrec()
+
+ local selfrest = {}
+ for index, expression in ipairs(self.expressions) do
+ if index > 1 then
+ selfrest[index - 1] = expression
+ end
+ end
+
+ local otherrest = {}
+ for index, expression in ipairs(other.expressions) do
+ if index > 1 then
+ otherrest[index - 1] = expression
+ end
+ end
+
+ if first.operation ~= BinaryOperation.ADD or not first.expressions[2] then
+ local result = BinaryOperation(self.operation, selfrest):mergesums(BinaryOperation(other.operation, otherrest))
+ if not first.expressions[1] then
+ return result
+ end
+ if first.expressions[1] ~= Integer.zero(0) then
+ table.insert(result.expressions, 1, first.expressions[1])
+ end
+ return result
+ end
+
+ local result
+ if first.expressions[1] == self.expressions[1] then
+ result = BinaryOperation(self.operation, selfrest):mergesums(other)
+ else
+ result = self:mergesums(BinaryOperation(other.operation, otherrest))
+ end
+
+ table.insert(result.expressions, 1, first.expressions[1])
+
+ return result
+end
diff --git a/macros/luatex/latex/luacas/tex/core/compoundexpression.lua b/macros/luatex/latex/luacas/tex/core/compoundexpression.lua
new file mode 100644
index 0000000000..792c36cfc2
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/compoundexpression.lua
@@ -0,0 +1,52 @@
+--- @class CompoundExpression
+--- Interface for an expression consisting of one or more subexpressions.
+CompoundExpression = {}
+__CompoundExpression = {}
+
+----------------------
+-- Instance methods --
+----------------------
+
+--- @param symbol SymbolExpression
+--- @return boolean
+function CompoundExpression:freeof(symbol)
+ for _, expression in ipairs(self:subexpressions()) do
+ if not expression:freeof(symbol) then
+ return false
+ end
+ end
+ return true
+ end
+
+--- @param map table<Expression, Expression>
+--- @return Expression
+function CompoundExpression:substitute(map)
+ for expression, replacement in pairs(map) do
+ if self == expression then
+ return replacement
+ end
+ end
+
+ local results = {}
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:substitute(map)
+ end
+ return self:setsubexpressions(results)
+end
+
+--- @return boolean
+function CompoundExpression:isatomic()
+ return false
+end
+
+--- @return boolean
+function CompoundExpression:isconstant()
+ return false
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__CompoundExpression.__index = Expression
+CompoundExpression = setmetatable(CompoundExpression, __CompoundExpression) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/constantexpression.lua b/macros/luatex/latex/luacas/tex/core/constantexpression.lua
new file mode 100644
index 0000000000..0f91a037a9
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/constantexpression.lua
@@ -0,0 +1,62 @@
+--- @class ConstantExpression
+--- @alias Constant ConstantExpression
+--- Interface for a mathematical expression without any symbols.
+--- ConstantExpressions are AtomicExpressions by default, but individual classes may overwrite that inheritance.
+ConstantExpression = {}
+__ConstantExpression = {}
+
+----------------------
+-- Instance methods --
+----------------------
+
+
+--- @param symbol SymbolExpression
+--- @return boolean
+function ConstantExpression:freeof(symbol)
+ return true
+end
+
+--- @return boolean
+function ConstantExpression:isconstant()
+ return true
+end
+
+--- @param other Expression
+--- @return boolean
+function ConstantExpression:order(other)
+
+ -- Constants come before non-constants.
+ if not other:isconstant() then
+ return true
+ end
+
+ if self ~= E and self ~= PI and self ~= I then
+ if other ~= E and other ~= PI and other ~= I then
+ -- If both self and other are ring elements, we use the total order on the ring to sort.
+ return self < other
+ end
+ -- Special constants come after ring elements.
+ return true
+ end
+
+ -- Special constants come after ring elements.
+ if other ~= E and other ~= PI and other ~= I then
+ return false
+ end
+
+ -- Ensures E < PI < I.
+
+ if self == E then return true end
+
+ if self == I then return false end
+
+ return other == I
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__ConstantExpression.__index = AtomicExpression
+ConstantExpression = setmetatable(ConstantExpression, __ConstantExpression)
+
diff --git a/macros/luatex/latex/luacas/tex/core/expression.lua b/macros/luatex/latex/luacas/tex/core/expression.lua
new file mode 100644
index 0000000000..8ff4659f79
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/expression.lua
@@ -0,0 +1,280 @@
+--- @class Expression
+--- Interface for an arbitrary mathematical expression.
+Expression = {}
+
+----------------------
+-- Required methods --
+----------------------
+
+--- Evaluates the current expression recursively by evaluating each sub-expression.
+--- @return Expression
+function Expression:evaluate()
+ error("Called unimplemented method : evaluate()")
+end
+
+--- Performs automatic simplification of an expression. Called on every expression before being output from the CAS.
+--- @return Expression
+function Expression:autosimplify()
+ error("Called unimplemented method : autosimplify()")
+end
+
+--- Performs more rigorous simplification of an expression. Checks different equivalent forms and determines the 'smallest' expresion.
+--- @return Expression
+function Expression:simplify()
+ local me = self:unlock():autosimplify()
+ local results = {}
+ for index, expression in ipairs(me:subexpressions()) do
+ results[index] = expression:simplify()
+ end
+ me = me:setsubexpressions(results)
+
+ local out = me
+ local minsize = self:size()
+
+ local test = me:expand()
+ if test:size() < minsize then
+ out = test
+ minsize = test:size()
+ end
+
+ test = me:factor()
+ if test:size() < minsize then
+ out = test
+ minsize = test:size()
+ end
+
+ return out
+end
+
+--- Changes the autosimplify behavior of an expression depending on its parameters.
+--- THIS METHOD MUTATES THE OBJECT IT ACTS ON.
+--- @param mode number
+--- @param permanent boolean
+--- @param recursive boolean
+--- @return Expression
+function Expression:lock(mode, permanent, recursive)
+ function self:autosimplify()
+ if not permanent then
+ self.autosimplify = nil
+ end
+
+ if mode == Expression.NIL then
+ return self
+ elseif mode == Expression.SUBS then
+ local results = {}
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:autosimplify()
+ end
+ self = self:setsubexpressions(results, true) -- TODO: Add mutate setsubexpressions
+ return self
+ end
+ end
+ if recursive then
+ for _, expression in ipairs(self:subexpressions()) do
+ expression:lock(mode, permanent, recursive)
+ end
+ end
+ return self
+end
+
+--- Frees any locks on expressions.
+--- THIS METHOD MUTATES THE OBJECT IT ACTS ON.
+--- @param recursive boolean
+--- @return Expression
+function Expression:unlock(recursive)
+ self.autosimplify = nil
+ if recursive then
+ for _, expression in ipairs(self:subexpressions()) do
+ expression:unlock(recursive)
+ end
+ end
+ return self
+end
+
+--- Returns a list of all subexpressions of an expression.
+--- @return table<number, Expression>
+function Expression:subexpressions()
+ error("Called unimplemented method : subexpressions()")
+end
+
+--- Returns the total number of atomic and compound expressions that make up an expression, or the number of nodes in the expression tree.
+--- @return Integer
+function Expression:size()
+ local out = Integer.one()
+ for _, expression in ipairs(self:subexpressions()) do
+ out = out + expression:size()
+ end
+ return out
+end
+
+--- Returns a copy of the original expression with each subexpression substituted with a new one, or a mutated version if mutate is true.
+--- @param subexpressions table<number, Expression>
+--- @param mutate boolean
+--- @return Expression
+function Expression:setsubexpressions(subexpressions, mutate)
+ error("Called unimplemented method : setsubexpressions()")
+end
+
+--- Determines whether or not an expression contains a particular symbol.
+--- @param symbol SymbolExpression
+--- @return boolean
+function Expression:freeof(symbol)
+ error("Called unimplemented method : freeof()")
+end
+
+--- Substitutes occurances of specified sub-expressions with other sub-expressions.
+--- @param map table<Expression, Expression>
+--- @return Expression
+function Expression:substitute(map)
+ error("Called unimplemented method : substitute()")
+end
+
+--- Algebraically expands an expression by turning products of sums into sums of products and expanding powers.
+--- @return Expression
+function Expression:expand()
+ return self
+end
+
+--- Attempts to factor an expression by turning sums of products into products of sums, and identical terms multiplied together into factors.
+--- @return Expression
+function Expression:factor()
+ return self
+end
+
+--- Attempts to combine an expression by collapsing sums of expressions together into a single factor, e.g. common denominator
+--- @return Expression
+function Expression:combine()
+ return self
+end
+
+--- Attempts to collect all occurances of an expression in this expression.
+--- @param collect Expression
+--- @return Expression
+function Expression:collect(collect)
+ return self
+end
+
+--- Returns all non-constant subexpressions of this expression - helper method for factor.
+--- @return table<number, Expression>
+function Expression:getsubexpressionsrec()
+ local result = {}
+
+ for _, expression in ipairs(self:subexpressions()) do
+ if not expression:isconstant() then
+ result[#result+1] = expression
+ end
+ result = JoinArrays(result, expression:getsubexpressionsrec())
+ end
+
+ return result
+end
+
+--- Determines whether an expression is atomic.
+--- Atomic expressions are not necessarily constant, since polynomial rings, for instance, are atomic parts that contain symbols.
+--- @return boolean
+function Expression:isatomic()
+ error("Called unimplemented method: isatomic()")
+end
+
+--- Determines whether an expression is a constant, i.e., an atomic expression that is not a varaible and cannot be converted into an equivalent compound expression.
+--- @return boolean
+function Expression:isconstant()
+ error("Called unimplemented method: isconstant()")
+end
+
+--- Determines whether an expression is a 'proper' real constant, i.e., is free of every varaible.
+function Expression:isrealconstant()
+ if self:isconstant() or self == PI or self == E then
+ return true
+ end
+
+ for _, expression in ipairs(self:subexpressions()) do
+ if not expression:isrealconstant() then
+ return false
+ end
+ end
+
+ return self:type() ~= SymbolExpression
+end
+
+--- Determines whether an expression is a 'proper' complex constant, i.e., is free of every varaible and is of the form a + bI for nonzero a and b.
+--- @return boolean
+function Expression:iscomplexconstant()
+ return self:isrealconstant() or (self.operation == BinaryOperation.ADD and #self.expressions == 2 and self.expressions[1]:isrealconstant()
+ and ((self.expressions[2].operation == BinaryOperation.MUL and #self.expressions[2].expressions == 2 and self.expressions[2].expressions[1]:isrealconstant() and self.expressions[2].expressions[2] == I)
+ or self.expressions[2] == I)) or (self.operation == BinaryOperation.MUL and #self.expressions == 2 and self.expressions[1]:isrealconstant() and self.expressions[2] == I)
+end
+
+--- A total order on autosimplified expressions. Returns true if self < other.
+--- @param other Expression
+--- @return boolean
+function Expression:order(other)
+ error("Called unimplemented method: order()")
+end
+
+--- Returns an autosimplified expression as a single-variable polynomial in a ring, if it can be converted. Returns itself otherwise.
+--- @return PolynomialRing, boolean
+function Expression:topolynomial()
+ return self, false
+end
+
+--- Converts this expression to LaTeX code.
+--- @return string
+function Expression:tolatex()
+ error("Called Unimplemented method: tolatex()")
+end
+
+----------------------
+-- Instance methods --
+----------------------
+
+--- Returns the type of the expression, i.e., the table used to create objects of that type.
+--- @return table
+function Expression:type()
+ return getmetatable(self).__index
+end
+
+--------------------------
+-- Instance metamethods --
+--------------------------
+
+__ExpressionOperations = {}
+
+__ExpressionOperations.__unm = function(a)
+ return BinaryOperation.SUBEXP({a})
+end
+
+__ExpressionOperations.__add = function(a, b)
+ return BinaryOperation.ADDEXP({a, b})
+end
+
+__ExpressionOperations.__sub = function(a, b)
+ return BinaryOperation.SUBEXP({a, b})
+end
+
+__ExpressionOperations.__mul = function(a, b)
+ return BinaryOperation.MULEXP({a, b})
+end
+
+__ExpressionOperations.__div = function(a, b)
+ return BinaryOperation.DIVEXP({a, b})
+end
+
+__ExpressionOperations.__pow = function(a, b)
+ return BinaryOperation.POWEXP({a, b})
+end
+
+-- For iterating over the subexpressions of a easily.
+__ExpressionOperations.__call = function(a, ...)
+ if a:type() == SymbolExpression then
+ return FunctionExpression(a, table.pack(...))
+ end
+ return BinaryOperation.MULEXP({a, table.pack(...)[1]})
+end
+
+----------------------
+-- Static constants --
+----------------------
+
+Expression.NIL = 0
+Expression.SUBS = 1 \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/functionexpression.lua b/macros/luatex/latex/luacas/tex/core/functionexpression.lua
new file mode 100644
index 0000000000..dc94790b81
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/functionexpression.lua
@@ -0,0 +1,295 @@
+--- @class FunctionExpression
+--- Represents a generic function that takes zero or more expressions as inputs.
+--- @field name SymbolExpression
+--- @field expressions table<number, Expression>
+--- @field orders table<number, Integer>
+--- @field variables table<number,SymbolExpression>
+--- @alias Function FunctionExpression
+FunctionExpression = {}
+__FunctionExpression = {}
+
+----------------------------
+-- Instance functionality --
+----------------------------
+
+--- Creates a new function expression with the given operation.
+--- @param name string|SymbolExpression
+--- @param expressions table<number, Expression>
+--- @param derivatives table<number,Integer>
+--- @return FunctionExpression
+function FunctionExpression:new(name, expressions, derivatives)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+
+ if type(name) == "table" and name:type() == SymbolExpression then
+ name = name.symbol
+ end
+
+ if TrigExpression.NAMES[name] and #expressions == 1 then
+ return TrigExpression(name, expressions[1])
+ end
+
+ -- TODO: Symbol Checking For Constructing derivatives like this
+ --if string.sub(name, #name, #name) == "'" and #expressions == 1 then
+ -- return DerivativeExpression(FunctionExpression(string.sub(name, 1, #name - 1), expressions), SymbolExpression("x"), true)
+ --end
+
+ o.name = name
+ o.expressions = Copy(expressions)
+ o.variables = Copy(expressions)
+ for _,expression in ipairs(o.variables) do
+ if not expression:isatomic() then
+ o.variables = {}
+ if #o.expressions < 4 then
+ local defaultvars = {SymbolExpression('x'),SymbolExpression('y'),SymbolExpression('z')}
+ for i=1,#o.expressions do
+ o.variables[i] = defaultvars[i]
+ end
+ else
+ for i=1,#o.expressions do
+ o.variables[i] = SymbolExpression('x_'..tostring(i))
+ end
+ end
+ end
+ end
+ if derivatives then
+ o.derivatives = Copy(derivatives)
+ else
+ o.derivatives = {}
+ for i=1,#o.variables do
+ o.derivatives[i] = Integer.zero()
+ end
+ end
+
+ __o.__index = FunctionExpression
+ __o.__tostring = function(a)
+ local total = Integer.zero()
+ for _,integer in ipairs(a.derivatives) do
+ total = total + integer
+ end
+ if total == Integer.zero() then
+ local out = a.name .. '('
+ for index, expression in ipairs(a.expressions) do
+ out = out .. tostring(expression)
+ if a.expressions[index + 1] then
+ out = out .. ', '
+ end
+ end
+ return out .. ')'
+ else
+ local out = 'd'
+ if total > Integer.one() then
+ out = out ..'^' .. tostring(total)
+ end
+ out = out .. a.name .. '/'
+ for index,integer in ipairs(a.derivatives) do
+ if integer > Integer.zero() then
+ out = out .. 'd' .. tostring(a.variables[index])
+ if integer > Integer.one() then
+ out = out .. '^' .. tostring(integer)
+ end
+ end
+ end
+ out = out .. '('
+ for index, expression in ipairs(a.expressions) do
+ out = out .. tostring(expression)
+ if a.expressions[index + 1] then
+ out = out .. ', '
+ end
+ end
+ return out .. ')'
+ end
+ end
+ __o.__eq = function(a, b)
+ -- if b:type() == TrigExpression then
+ -- return a == b:tofunction()
+ -- end
+ if b:type() ~= FunctionExpression then
+ return false
+ end
+ if #a.expressions ~= #b.expressions then
+ return false
+ end
+ for index, _ in ipairs(a.expressions) do
+ if a.expressions[index] ~= b.expressions[index] then
+ return false
+ end
+ end
+ for index,_ in ipairs(a.derivatives) do
+ if a.derivatives[index] ~= b.derivatives[index] then
+ return false
+ end
+ end
+ return a.name == b.name
+ end
+
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- @return FunctionExpression
+function FunctionExpression:evaluate()
+ local results = {}
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:evaluate()
+ end
+ local result = FunctionExpression(self.name, results, self.derivatives)
+ result.variables = self.variables
+ return result
+end
+
+--- @return FunctionExpression
+function FunctionExpression:autosimplify()
+ -- Since the function is completely generic, we can't really do anything execpt autosimplify subexpressions.
+ local results = {}
+ for index, expression in ipairs(self:subexpressions()) do
+ results[index] = expression:autosimplify()
+ end
+ local result = FunctionExpression(self.name, results, self.derivatives)
+ result.variables = self.variables
+ return result
+end
+
+--- @return table<number, Expression>
+function FunctionExpression:subexpressions()
+ return self.expressions
+end
+
+--- @param subexpressions table<number, Expression>
+--- @return FunctionExpression
+function FunctionExpression:setsubexpressions(subexpressions)
+ local result = FunctionExpression(self.name, subexpressions, self.derivatives)
+ result.variables = self.variables
+ return result
+end
+
+--- @param other Expression
+--- @return boolean
+function FunctionExpression:order(other)
+ if other:isatomic() then
+ return false
+ end
+
+ -- CASC Autosimplfication has some symbols appearing before functions, but that looks bad to me, so all symbols appear before products now.
+ -- if other:type() == SymbolExpression then
+ -- return SymbolExpression(self.name):order(other)
+ -- end
+
+ if other:type() == BinaryOperation then
+ if other.operation == BinaryOperation.ADD or other.operation == BinaryOperation.MUL then
+ return BinaryOperation(other.operation, {self}):order(other)
+ end
+
+ if other.operation == BinaryOperation.POW then
+ return (self^Integer.one()):order(other)
+ end
+ end
+
+ if other:type() == SqrtExpression then
+ return self:order(other:topower())
+ end
+
+ -- TODO: Make Logarithm and AbsExpression inherit from function expression to reduce code duplication
+ if other:type() == Logarithm then
+ return self:order(FunctionExpression("log", {other.base, other.expression}))
+ end
+
+ if other:type() ~= FunctionExpression and other:type() ~= TrigExpression then
+ return true
+ end
+
+ if self.name ~= other.name then
+ return SymbolExpression(self.name):order(SymbolExpression(other.name))
+ end
+
+ local k = 1
+ while self:subexpressions()[k] and other:subexpressions()[k] do
+ if self:subexpressions()[k] ~= other:subexpressions()[k] then
+ return self:subexpressions()[k]:order(other:subexpressions()[k])
+ end
+ k = k + 1
+ end
+ return #self.expressions < #other.expressions
+end
+
+--- @return string
+function FunctionExpression:tolatex()
+ local out = tostring(self.name)
+ if self:type() == TrigExpression then
+ out = "\\" .. out
+ end
+ if self:type() ~= TrigExpression and #self.name>1 then
+ --if out:sub(2,2) ~= "'" then
+ --local fp = out:find("'")
+ --if fp then
+ -- out = '\\operatorname{' .. out:sub(1,fp-1) .. '}' .. out:sub(fp,-1)
+ --else
+ out = '\\operatorname{' .. out .. '}'
+ --end
+ --end
+ end
+ local total = Integer.zero()
+ for _,integer in ipairs(self.derivatives) do
+ total = total + integer
+ end
+ if #self.expressions == 1 then
+ if total == Integer.zero() then
+ goto continue
+ else
+ if total < Integer(5) then
+ while total > Integer.zero() do
+ out = out .. "'"
+ total = total - Integer.one()
+ end
+ else
+ out = out .. '^{(' .. total:tolatex() .. ')}'
+ end
+ end
+ end
+ if #self.expressions > 1 then
+ if total == Integer.zero() then
+ goto continue
+ else
+ if total < Integer(4) then
+ out = out .. '_{'
+ for index,integer in ipairs(self.derivatives) do
+ local i = integer:asnumber()
+ while i > 0 do
+ out = out .. self.variables[index]:tolatex()
+ i = i - 1
+ end
+ end
+ out = out .. '}'
+ else
+ out = '\\frac{\\partial^{' .. total:tolatex() .. '}' .. out .. '}{'
+ for index, integer in ipairs(self.derivatives) do
+ if integer > Integer.zero() then
+ out = out .. '\\partial ' .. self.variables[index]:tolatex()
+ if integer ~= Integer.one() then
+ out = out .. '^{' .. integer:tolatex() .. '}'
+ end
+ end
+ end
+ out = out .. '}'
+ end
+ end
+ end
+ ::continue::
+ out = out ..'\\mathopen{}' .. '\\left('
+ for index, expression in ipairs(self:subexpressions()) do
+ out = out .. expression:tolatex()
+ if self:subexpressions()[index + 1] then
+ out = out .. ', '
+ end
+ end
+ return out .. '\\right)'
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__FunctionExpression.__index = CompoundExpression
+__FunctionExpression.__call = FunctionExpression.new
+FunctionExpression = setmetatable(FunctionExpression, __FunctionExpression) \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/core/symbolexpression.lua b/macros/luatex/latex/luacas/tex/core/symbolexpression.lua
new file mode 100644
index 0000000000..0d9192c084
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/core/symbolexpression.lua
@@ -0,0 +1,132 @@
+--- @class SymbolExpression
+--- An atomic expression corresponding to a symbol representing an arbitary value.
+--- @field symbol string
+--- @alias Symbol SymbolExpression
+SymbolExpression = {}
+__SymbolExpression = {}
+
+----------------------
+-- Instance methods --
+----------------------
+
+--- Given the name of the symbol as a string, creates a new symbol.
+--- @param symbol string
+--- @return SymbolExpression
+function SymbolExpression:new(symbol)
+ local o = {}
+ local __o = Copy(__ExpressionOperations)
+ __o.__index = SymbolExpression
+ __o.__tostring = function(a)
+ return a.symbol
+ end
+ __o.__eq = function(a, b)
+ return a.symbol == b.symbol
+ end
+
+ if type(symbol) ~= "string" then
+ error("Sent parameter of wrong type: symbol must be a string")
+ end
+
+ o.symbol = symbol
+ o = setmetatable(o, __o)
+
+ return o
+end
+
+--- @return boolean
+function SymbolExpression:freeof(symbol)
+ return symbol~=self
+end
+
+--- @return boolean
+function SymbolExpression:isconstant()
+ return false
+end
+
+--- @param other Expression
+--- @return boolean
+function SymbolExpression:order(other)
+
+ -- Symbol Expressions come after constant expressions.
+ if other:isconstant() then
+ return false
+ end
+
+ -- Lexographic order on symbols.
+ if other:type() == SymbolExpression then
+ for i = 1, math.min(#self.symbol, #other.symbol) do
+ if string.byte(self.symbol, i) ~= string.byte(other.symbol, i) then
+ return string.byte(self.symbol, i) < string.byte(other.symbol, i)
+ end
+ end
+
+ return #self.symbol < #other.symbol
+ end
+
+ if other.operation == BinaryOperation.POW then
+ return BinaryOperation(BinaryOperation.POW, {self, Integer.one()}):order(other)
+ end
+
+ if other.operation == BinaryOperation.MUL then
+ return BinaryOperation(BinaryOperation.MUL, {self}):order(other)
+ end
+
+ if other.operation == BinaryOperation.ADD then
+ return BinaryOperation(BinaryOperation.ADD, {self}):order(other)
+ end
+
+ -- CASC Autosimplfication has some symbols appearing before functions, but that looks bad to me, so all symbols appear before products now.
+ if other:type() == FunctionExpression or other:type() == TrigExpression or other:type() == Logarithm then
+ return true
+ end
+
+ return false
+end
+
+--- Converts this symbol to an element of a polynomial ring.
+--- @return PolynomialRing, boolean
+function SymbolExpression:topolynomial()
+ return PolynomialRing({Integer.zero(), Integer.one()}, self.symbol), true
+end
+
+-----------------
+-- Inheritance --
+-----------------
+
+__SymbolExpression.__index = AtomicExpression
+__SymbolExpression.__call = SymbolExpression.new
+SymbolExpression = setmetatable(SymbolExpression, __SymbolExpression)
+
+
+----------------------
+-- Static Constants --
+----------------------
+
+-- The constant pi.
+PI = SymbolExpression("pi")
+function PI:tolatex()
+ return "\\pi "
+end
+
+-- Approximates pi as a rational number. Uses continued fraction expansion.
+function PI:approximate()
+ return Integer(313383936) / Integer(99753205)
+end
+
+--function PI:isconstant()
+-- return true
+--end
+
+-- The constant e.
+E = SymbolExpression("e")
+
+-- Approximates pi as a rational number. Uses continued fraction expansion.
+function E:approximate()
+ return Integer(517656) / Integer(190435)
+end
+
+-- The imaginary constant i.
+I = SymbolExpression("i")
+function I:tolatex()
+ return "i"
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/luacas.sty b/macros/luatex/latex/luacas/tex/luacas.sty
new file mode 100644
index 0000000000..461fae6efa
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/luacas.sty
@@ -0,0 +1,312 @@
+% Permission is granted to copy, distribute and/or modify this
+% software under the terms of the LaTeX Project Public License
+% (LPPL), version 1.3c or any later version.
+%
+% This software is provided 'as is', without warranty of any kind,
+% either expressed or implied, including, but not limited to, the
+% implied warranties of merchantability and fitness for a
+% particular purpose.
+
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{luacas}
+ [2022/05/07 v1.0 CAS written in Lua for LaTeX]
+
+\RequirePackage{iftex}
+\ifluatex
+ \RequirePackage{luacode}
+\else
+ {\PackageError{luacas}
+ {Not running under LuaLaTeX}
+ {This package requires LuaLaTeX. Try compiling this document with\MessageBreak 'lualatex' instead of 'latex'. This is a fatal error; I'm aborting now.}%
+ }\stop
+\fi
+
+%Required Packages
+\RequirePackage{xparse}
+\RequirePackage{pgfkeys}
+\RequirePackage{verbatim}
+\RequirePackage{tikz}
+\RequirePackage{xcolor}
+\RequirePackage{mathtools}
+
+%These files contain Lua code for parsing luacas output; they also initialize the CAS itself
+\directlua{require('test.parser')
+ require('test.helper')
+}
+
+\NewDocumentEnvironment{CAS}%
+ {+b}%
+ {\luaexec{CASparse([[#1]])}}%
+ {}
+
+\newcommand{\get}%
+ [2][true]%
+ {\directlua{disp(#2, #1)}}
+
+\newcommand{\fetch}[1]{
+ \directlua{tex.print(tostring(#1))}
+}
+
+\NewDocumentCommand{\store}{m O{#1}}{
+ \expandafter\def\csname #2\endcsname{%
+ \directlua{
+ input = #1
+ if not input[1] then
+ tex.sprint{tostring(input)}
+ else
+ tex.sprint("{")
+ for _,entry in ipairs(input) do
+ tex.sprint(tostring(entry),",")
+ end
+ tex.sprint("}")
+ end
+ }%
+ }%
+}%
+
+\NewDocumentCommand{\yoink}{m O{#1}}{%
+\expandafter\def\csname #2\endcsname{%
+ \directlua{
+ tex.print(tostring(#1))
+ }%
+ }%
+}
+
+%\newcommand{\eval}[1]{\luaexec{tex.print(parse('#1'):tolatex())}}
+
+%%%%%%%%%%
+%% Core %%
+%%%%%%%%%%
+
+%pretty print
+
+\NewDocumentCommand{\print}{s m}{%
+ \IfBooleanTF{#1}{%
+ \directlua{
+ local sym = #2
+ if sym then
+ tex.print(sym:autosimplify():tolatex())
+ else
+ tex.print('nil')
+ end
+ }%
+ }{%
+ \directlua{
+ local sym = #2
+ if sym then
+ tex.print(sym:tolatex())
+ else
+ tex.print('nil')
+ end
+ }%
+ }%
+}
+
+%verbatim print
+
+\NewDocumentCommand{\vprint}{s m}{%
+ \IfBooleanTF{#1}{%
+ \directlua{
+ local sym = #2
+ tex.sprint([[\unexpanded{\begin{verbatim}]]..tostring(sym)..[[\end{verbatim}}]])
+ }%
+ }{%
+ \directlua{
+ local sym = #2
+ tex.sprint([[\unexpanded{\begin{verbatim}]]..tostring(sym:autosimplify())..[[\end{verbatim}}]])
+ }%
+ }%
+}
+
+\NewDocumentCommand{\lprint}{m O{nil,nil}}{%
+ \luaexec{
+ local tbl = #1
+ local low,upp = #2
+ local tmp =0
+ if tbl[0] == nil then
+ tmp = 1
+ end
+ upp = upp or \#tbl
+ low = low or tmp
+ for i=low,upp do
+ tex.print(tbl[i]:tolatex())
+ if tbl[i+1] then
+ tex.print(",")
+ end
+ end
+ }
+}
+
+%prints the first level of an expression tree; for use within a tikzpicture environment
+
+\NewDocumentCommand{\printshrub}{s m}{%
+ \IfBooleanTF{#1}{%
+ \directlua{
+ local sym = #2
+ sym = sym:autosimplify()
+ tex.print("\\node [label=90:",whatis(sym),"] {",nameof(sym),"}")
+ tex.print(sym:gettheshrub())
+ tex.print(";")
+ }%
+ }{%
+ \directlua{
+ local sym = #2
+ tex.print("\\node [label=90:",whatis(sym),"] {",nameof(sym),"}")
+ tex.print(sym:gettheshrub())
+ tex.print(";")
+ }%
+ }
+}
+
+%prints the full expression tree; for use within a tikzpicture environment
+
+\NewDocumentCommand{\printtree}{s m}{%
+ \IfBooleanTF{#1}{%
+ \luaexec{
+ local sym = #2
+ sym = sym:autosimplify()
+ tex.print("\\node {",nameof(sym),"}")
+ tex.print(sym:getthetree())
+ tex.print(";")
+ }%
+ }{%
+ \luaexec{
+ local sym = #2
+ tex.print("\\node {",nameof(sym),"}")
+ tex.print(sym:getthetree())
+ tex.print(";")
+ }%
+ }
+}
+
+%parses an expression tree for use within the forest environment; result is stored in \forestresult
+
+\NewDocumentCommand{\parseforest}{s m}{%
+ \IfBooleanTF{#1}{%
+ \luaexec{
+ local sym = #2
+ sym = sym:autosimplify()
+ tex.print("\\def\\forestresult{")
+ tex.print("[")
+ tex.print(nameof(sym))
+ tex.print(sym:gettheforest())
+ tex.print("]")
+ tex.print("}")
+ }%
+ }{%
+ \luaexec {
+ local sym = #2
+ tex.print("\\def\\forestresult{")
+ tex.print("[")
+ tex.print(nameof(sym))
+ tex.print(sym:gettheforest())
+ tex.print("]")
+ tex.print("}")
+ }%
+ }
+}
+
+\NewDocumentCommand{\parseshrub}{s m}{%
+ \IfBooleanTF{#1}{%
+ \luaexec{
+ local sym = #2
+ sym = sym:autosimplify()
+ tex.print("\\def\\shrubresult{")
+ tex.print("[")
+ tex.print(nameof(sym))
+ tex.print(", tikz+={\\node[anchor=south] at (.north) {test};}")
+ tex.print(sym:getthefancyshrub())
+ tex.print("]")
+ tex.print("}")
+ }%
+ }{%
+ \luaexec{
+ local sym = #2
+ tex.print("\\def\\shrubresult{")
+ tex.print("[")
+ tex.print(nameof(sym))
+ tex.print(", tikz+={\\node[anchor=south,font=\\ttfamily\\footnotesize,gray] at (.north) {",longwhatis(sym),"};}")
+ tex.print(sym:getthefancyshrub())
+ tex.print("]")
+ tex.print("}")
+ }%
+ }
+}
+
+\NewDocumentCommand{\whatis}{m}{%
+ \luaexec{
+ tex.sprint("{\\ttfamily",longwhatis(#1),"}")
+ }%
+}
+
+\NewDocumentCommand{\freeof}{s m m}{%
+ \IfBooleanTF{#1}{%
+ \luaexec{
+ local sym1 = #2
+ local sym2 = #3
+ if sym1:freeof(sym2) then
+ tex.print(1)
+ else
+ tex.print(0)
+ end
+ }
+ }{%
+ \luaexec{
+ local sym1 = #2
+ local sym2 = #3
+ sym1 = sym1:autosimplify()
+ sym2 = sym2:autosimplify()
+ if sym1:freeof(sym2) then
+ tex.print(1)
+ else
+ tex.print(0)
+ end
+ }%
+ }%
+}
+
+\NewDocumentCommand{\isatomic}{s m}{%
+ \IfBooleanTF{#1}{
+ \luaexec{
+ local sym = #2
+ if sym:isatomic() then
+ tex.print(1)
+ else
+ tex.print(0)
+ end
+ }
+ }{%
+ \luaexec{
+ local sym = #2
+ sym = sym:autosimplify()
+ if sym:isatomic() then
+ tex.print(1)
+ else
+ tex.print(0)
+ end
+ }%
+ }%
+}
+
+\NewDocumentCommand{\isconstant}{s m}{%
+ \IfBooleanTF{#1}{%
+ \luaexec{
+ local sym = #2
+ if sym:isconstant() then
+ tex.print(1)
+ else
+ tex.print(0)
+ end
+ }%
+ }{%
+ \luaexec{%
+ local sym = #2
+ sym = sym:autosimplify()
+ if sym:isconstant() then
+ tex.print(1)
+ else
+ tex.print(0)
+ end
+ }%
+ }%
+}
diff --git a/macros/luatex/latex/luacas/tex/test/calculus/derivatives.lua b/macros/luatex/latex/luacas/tex/test/calculus/derivatives.lua
new file mode 100644
index 0000000000..5d5429f59b
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/calculus/derivatives.lua
@@ -0,0 +1,30 @@
+local a = DD(SymbolExpression("x") * SymbolExpression("y"), SymbolExpression("x"))
+local b = DD(Integer(3) * SymbolExpression("x") ^ Integer(2) + Integer(2) * SymbolExpression("x") + Integer(6), SymbolExpression("x"))
+local c = DD(E ^ SymbolExpression("x"), SymbolExpression("x"))
+local d = DD(FunctionExpression("f", {SymbolExpression("x") ^ Integer(2)}))
+local e = DD(SymbolExpression("x") ^ SymbolExpression("x"))
+local f = DD(PolynomialRing({Integer(3), Integer(4), Integer(5)}, "x"))
+local g = DD(LN(SymbolExpression("y")), SymbolExpression("y"))
+local h = DD(SymbolExpression("x") ^ SymbolExpression("n"))
+local i = DD(SIN((SymbolExpression("x"))))
+local j = DD(SIN(Integer(2) * COS(SymbolExpression("x"))))
+local k = DD(ARCTAN(SymbolExpression("x") ^ (Integer(1) / Integer(2))))
+local l = DD(ARCSEC(SymbolExpression("x")))
+
+starttest("derivatives")
+
+testeq(a, dparse("DD(x*y, x)"))
+testeq(a:autosimplify(), parse("y"), a)
+testeq(b:autosimplify(), parse("6 * x + 2"), b)
+testeq(c:autosimplify(), parse("e^x"), c)
+testeq(d:autosimplify(), (Integer(2) * SymbolExpression("x") * FunctionExpression("f", {SymbolExpression("x")^Integer(2)}, {Integer(1)})):autosimplify(), d)
+testeq(e:autosimplify(), parse("x^x * (1 + ln(x))"), e)
+testeq(f:autosimplify(), parse("4 + 10 * x"), f)
+testeq(g:autosimplify(), parse("y ^ -1"), g)
+testeq(h:autosimplify(), parse("n * x ^ (-1 + n)"), h)
+testeq(i:autosimplify(), parse("cos(x)"), i)
+testeq(j:autosimplify(), parse("-2 * cos(2 * cos(x)) * sin(x)"), j)
+testeq(k:autosimplify(), parse("1/2 * x^(-1/2) * (1+x) ^ -1"), k)
+testeq(l:autosimplify(), parse("abs(x)^-1 * (1 + -(x^2))^ (-1/2)"), l)
+
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/calculus/integrals.lua b/macros/luatex/latex/luacas/tex/test/calculus/integrals.lua
new file mode 100644
index 0000000000..0c12f7a787
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/calculus/integrals.lua
@@ -0,0 +1,54 @@
+local a = dparse("int(x^2, x)")
+local b = dparse("int(x^-1, x, 1, e)")
+local c = dparse("int(3*x^2+2*x+6, x)")
+local d = dparse("int(sin(x)*cos(x), x)")
+local e = dparse("int(2*x*cos(x^2), x)")
+local f = dparse("int(sin(2*x), x)")
+local g = dparse("int(e^sin(x), x)")
+local h = dparse("int((1 / (1 + (1 / x))), x)")
+local i = dparse("int(e^(x^(1/2)), x)")
+local j = dparse("int((x^3+1)/(x-2), x)")
+local k = dparse("int((x^2-x+1)/(x^3+3*x^2+3*x+1), x)")
+local l = dparse("int(1 / (x^3+6*x), x)")
+local m = dparse("int(1/(x^2+x+1), x)")
+local n = dparse("int(1/(x^3+2*x+2), x, 0, 1)")
+
+local o = dparse("int(x^2*e^x, x)")
+local p = dparse("int((x^2+6*x+3)*sin(x), x)")
+local q = dparse("int(x*e^x*sin(x),x)")
+local r = dparse("int(cos(x)^3, x)")
+local s = dparse("int(1/(e^x+1), x)")
+local t = dparse("int(e^(2*x)*cos(3*x), x)")
+local u = dparse("int((x^2-1)^2, x, -1, 1)")
+
+starttest("integration")
+testeq(a, dparse("int(x ^ 2, x)"))
+testeq(a:autosimplify(), parse("x^3/3"), a)
+testeq(b:autosimplify(), parse("1"), b)
+testeq(c:autosimplify(), parse("x^3+x^2+6*x"), c)
+testeq(d:autosimplify(), parse("(-1/2 * (cos(x) ^ 2))"), d)
+testeq(e:autosimplify(), parse("sin((x ^ 2))"), e)
+testeq(f:autosimplify(), parse("(-1/2 * cos((2 * x)))"), f)
+-- testeq(g:autosimplify(), dparse("int(e ^ (sin(x)), x)"), g)
+testeq(h:autosimplify(), parse("x + (-1 * (log(e, 1 + (x ^ -1)))) + (-1 * (log(e, x)))"), h)
+testeq(i:autosimplify(), parse("-2 * (e ^ (x ^ (1/2))) + 2 * (e ^ (x ^ (1/2))) * (x ^ (1/2))"), i)
+testeq(j:autosimplify(), parse("((4 * x) + (x ^ 2) + (1/3 * (x ^ 3)) + (9 * log(e, (-2 + x))))"), j)
+testeq(k:autosimplify(), parse("((-3/2 * ((1 + x) ^ -2)) + (3 * ((1 + x) ^ -1)) + log(e, (1 + x)))"), k)
+testeq(l:autosimplify(), parse("((1/6 * log(e, x)) + (-1/12 * log(e, (6 + (x ^ 2)))))"), l)
+testeq(m:autosimplify(), parse("2/3 * (3 ^ (1/2)) * (arctan((3 ^ (1/2)) * (1/3 + (2/3 * x))))"), m)
+-- test(n:autosimplify(), [[((((6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)))
+-- * log(e, (18 * (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-264600 + (1/2
+-- * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/140 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1)))) + (((6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((-1/840 + (1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (18 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/280 + (-1/280 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1)))) + (((6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (18 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/140 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1)))) + (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (1 + (18 * (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/140 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1))))) + (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (1 + (18 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + ((1/280 + (-1/280 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -1) * ((-264600 + (1/2 * (216040608000
+-- ^ 1/2))) ^ -1/3)) + ((1/840 + (-1/840 * (-3 ^ 1/2))) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1))))) + (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) *
+-- ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) * log(e, (1 + (18 *
+-- (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2) * (((-18 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/140 * ((-1/2 + (1/2 * (-3 ^ 1/2)))
+-- ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3)) + (12 * (((-6 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ -2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ -1/3)) + (-1/420 * ((-1/2 + (1/2 * (-3 ^ 1/2))) ^ 2) * ((-264600 + (1/2 * (216040608000 ^ 1/2))) ^ 1/3))) ^ 2))) ^ -1))))))]], n)
+
+testeq(o:autosimplify(), parse("((2 * (e ^ x)) + (-2 * (e ^ x) * x) + ((e ^ x) * (x ^ 2)))"), o)
+testeq(p:autosimplify(), parse("((2 * cos(x)) + ((-3 + (-6 * x) + (-1 * (x ^ 2))) * cos(x)) + ((6 + (2 * x)) * sin(x)))"), p)
+testeq(q:autosimplify(), parse("(1/2 * (e ^ x) * (cos(x))) + (-1/2 * (e ^ x) * x * (cos(x))) + (1/2 * (e ^ x) * x * (sin(x)))"), q)
+testeq(r:autosimplify(), parse("((3/4 * sin(x)) + (1/12 * sin((3 * x))))"), r)
+testeq(s:autosimplify(), parse("log(e, 1 + (-1 * ((1 + (e ^ x)) ^ -1)))"), s)
+testeq(t:autosimplify(), parse("(2/13 * (e ^ (2 * x)) * (cos(3 * x))) + (3/13 * (e ^ (2 * x)) * (sin(3 * x)))"), t)
+testeq(u:autosimplify(), parse("16/15"), u)
+endtest()
diff --git a/macros/luatex/latex/luacas/tex/test/expressions/autosimplify.lua b/macros/luatex/latex/luacas/tex/test/expressions/autosimplify.lua
new file mode 100644
index 0000000000..09ad9874cd
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/expressions/autosimplify.lua
@@ -0,0 +1,199 @@
+local a = BinaryOperation.ADDEXP
+ ({Integer(3),
+ Integer(5)})
+
+local b = BinaryOperation.MULEXP
+ ({BinaryOperation.ADDEXP
+ ({Integer(13),
+ Integer(12)}),
+ Integer(-4)})
+
+local c = BinaryOperation.DIVEXP
+ ({SymbolExpression("x"),
+ SymbolExpression("y")})
+
+local d = BinaryOperation.DIVEXP
+ ({BinaryOperation.ADDEXP
+ ({Integer(4),
+ Integer(-3)}),
+ SymbolExpression("y")})
+
+local e = BinaryOperation.ADDEXP
+ ({Integer(3),
+ Integer(4),
+ Integer(5),
+ Integer(6)})
+
+starttest("expression construction")
+testeq(a, "3 + 5")
+testeq(b, "(13 + 12) * -4")
+testeq(c, "x / y")
+testeq(d, "(4 + -3) / y")
+testeq(e, "3 + 4 + 5 + 6")
+endtest()
+
+starttest("expression evaluation...")
+testeq(a:evaluate(), dparse("8"))
+testeq(b:evaluate(), dparse("-100"))
+testeq(c:evaluate(), dparse("(x / y)"))
+testeq(d:evaluate(), dparse("(1 / y)"))
+testeq(e:evaluate(), dparse("18"))
+endtest()
+
+local g = BinaryOperation.POWEXP
+ ({Integer(0),
+ SymbolExpression("x")})
+
+local h = BinaryOperation.POWEXP
+ ({Integer(1),
+ SymbolExpression("x")})
+
+local i = BinaryOperation.POWEXP
+ ({SymbolExpression("x"),
+ Integer(0)})
+
+local j = BinaryOperation.POWEXP
+ ({SymbolExpression("x"),
+ Integer(1)})
+
+local k = BinaryOperation.POWEXP
+ ({SymbolExpression("x"),
+ SymbolExpression("y")})
+
+local l = BinaryOperation.POWEXP
+ ({BinaryOperation.POWEXP
+ ({BinaryOperation.POWEXP
+ ({SymbolExpression("x"),
+ Integer(3)}),
+ Integer(4)}),
+ Integer(5)})
+
+local m = BinaryOperation.POWEXP
+ ({BinaryOperation.MULEXP
+ ({SymbolExpression("x"),
+ SymbolExpression("y")}),
+ SymbolExpression("a")})
+
+ local n = BinaryOperation.MULEXP
+ ({SymbolExpression("x"),
+ SymbolExpression("y"),
+ Integer(0),
+ Integer(-2)
+ })
+
+local o = BinaryOperation.MULEXP
+ ({SymbolExpression("x"),
+ BinaryOperation.MULEXP
+ ({SymbolExpression("y"),
+ SymbolExpression("z")})})
+
+local p = BinaryOperation.MULEXP
+ ({SymbolExpression("x")})
+
+local q = BinaryOperation.MULEXP
+ ({SymbolExpression("x"), SymbolExpression("x"), SymbolExpression("x"), SymbolExpression("x")})
+
+local r = BinaryOperation.MULEXP
+ ({SymbolExpression("x"), Integer(3), SymbolExpression("a")})
+
+ local s = BinaryOperation.ADDEXP
+ ({SymbolExpression("x")})
+
+local t = BinaryOperation.ADDEXP
+ ({SymbolExpression("x"),
+ BinaryOperation.ADDEXP
+ ({Integer(3),
+ SymbolExpression("y")})})
+
+local u = BinaryOperation.ADDEXP
+ ({BinaryOperation.MULEXP
+ ({SymbolExpression("x"),
+ SymbolExpression("y")}),
+ BinaryOperation.MULEXP
+ ({SymbolExpression("y"),
+ SymbolExpression("x")})})
+
+local v = BinaryOperation.ADDEXP
+ ({Integer(3),
+ BinaryOperation.ADDEXP
+ ({BinaryOperation.MULEXP
+ ({Integer(2),
+ BinaryOperation.POWEXP
+ ({SymbolExpression("x"),
+ Integer(2)})}),
+ BinaryOperation.ADDEXP
+ ({BinaryOperation.MULEXP
+ ({Integer(1),
+ SymbolExpression("y")}),
+ BinaryOperation.MULEXP
+ ({Integer(0),
+ SymbolExpression("x")})})}),
+ Integer(6)})
+
+ local w = BinaryOperation.MULEXP
+ ({BinaryOperation.DIVEXP
+ ({Integer(1),
+ SymbolExpression("x")}),
+ SymbolExpression("x")})
+
+local x = BinaryOperation.MULEXP
+ ({BinaryOperation.DIVEXP
+ ({SymbolExpression("y"),
+ SymbolExpression("x")}),
+ BinaryOperation.DIVEXP
+ ({SymbolExpression("x"),
+ SymbolExpression("y")})})
+
+local y = BinaryOperation.MULEXP
+ ({BinaryOperation.DIVEXP
+ ({Integer(1),
+ Integer(3)}),
+ SymbolExpression("x")})
+
+local z = BinaryOperation.ADDEXP
+ ({SymbolExpression("x"),
+ SymbolExpression("y"),
+ BinaryOperation.SUBEXP
+ ({SymbolExpression("x"),
+ SymbolExpression("y")})})
+
+local A = dparse("(-aa-x)+(x+aa)")
+
+starttest("expression autosimplification")
+testeq(g:autosimplify(), parse("0"), g)
+testeq(h:autosimplify(), parse("1"), h)
+testeq(i:autosimplify(), parse("1"), i)
+testeq(j:autosimplify(), parse("x"), j)
+testeq(k:autosimplify(), parse("x ^ y"), k)
+testeq(l:autosimplify(), parse("(x ^ 60)"), l)
+testeq(m:autosimplify(), parse("((x * y) ^ a)"), m)
+testeq(n:autosimplify(), parse("0"), n)
+testeq(o:autosimplify(), parse("(x * y * z)"), o)
+testeq(p:autosimplify(), parse("x"), p)
+testeq(q:autosimplify(), parse("(x ^ 4)"), q)
+testeq(r:autosimplify(), parse("(3 * a * x)"), r)
+testeq(s:autosimplify(), parse("x"), s)
+testeq(t:autosimplify(), parse("(3 + x + y)"), t)
+testeq(u:autosimplify(), parse("(2 * x * y)"), u)
+testeq(v:autosimplify(), parse("(9 + (2 * (x ^ 2)) + y)"), v)
+testeq(w:autosimplify(), parse("1"), w)
+testeq(x:autosimplify(), parse("1"), x)
+testeq(y:autosimplify(), parse("(1/3 * x)"), y)
+testeq(z:autosimplify(), parse("(2 * x)"), z)
+testeq(A:autosimplify(), parse("0"), A)
+endtest()
+
+
+local aa = SymbolExpression("x") + SymbolExpression("y") + SymbolExpression("z")
+local ab = -(SymbolExpression("x") / SymbolExpression("y"))
+local ac = Integer(2)*SymbolExpression("x")*SymbolExpression("y") - Integer(3)*SymbolExpression("x")*SymbolExpression("z")
+
+starttest("metamethod expressions")
+
+testeq(aa, dparse("(x + y) + z"))
+testeq(aa:autosimplify(), parse("(x + y + z)"), aa)
+testeq(ab, dparse("- (x / y)"))
+testeq(ab:autosimplify(), parse("(-1 * x * (y ^ -1))"), ab)
+testeq(ac:autosimplify(), parse("((2 * x * y) + (-3 * x * z))"), ac)
+
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/expressions/collect.lua b/macros/luatex/latex/luacas/tex/test/expressions/collect.lua
new file mode 100644
index 0000000000..b40e8e3d09
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/expressions/collect.lua
@@ -0,0 +1,26 @@
+local x = SymbolExpression("x")
+local ex = parse("e^x")
+local lnx = parse("ln(x)")
+
+local a = parse("y^2")
+local b = parse("x + y + 1")
+local c = parse("x*(y+1)+x+3*x*y^2")
+local d = parse("x^2+2*x*y+y^2+x")
+local e = parse("(x*y+x)^2+x^2")
+local f = parse("-x^2/e^x-2*x/e^x-2/e^x+x^2*e^x-2*x*e^x+2*e^x")
+local g = parse("x^(-2)+y*x^(-2)+z*x^2+2*x^2")
+local h = parse("a*ln(x)-ln(x)*x-x")
+
+
+starttest("collect method")
+
+testeq(a:collect(x), parse("y^2"), a)
+testeq(b:collect(x), parse("x + y + 1"), b)
+testeq(c:collect(x), parse("(3*y^2+y+2)*x"), c)
+testeq(d:collect(x), parse("x^2+(2*y+1)*x+y^2"), d)
+testeq(e:collect(x), parse("((y+1)^2+1)*x^2"), e)
+testeq(f:collect(ex), parse("(x^2-2*x+2)*e^x+(-x^2-2*x-2)/e^x"), f)
+testeq(g:collect(x), parse("(y+1)*x^(-2)+(z+2)*x^2"), g)
+testeq(h:collect(lnx), parse("(a-x)*ln(x)-x"), h)
+
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/expressions/equations.lua b/macros/luatex/latex/luacas/tex/test/expressions/equations.lua
new file mode 100644
index 0000000000..3d6aa131fc
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/expressions/equations.lua
@@ -0,0 +1,17 @@
+local a = Equation(parse("2^x"), parse("1"))
+local b = Equation(parse("x^2+2*x+1"), parse("0"))
+local c = Equation(parse("2*x^x"), parse("3*y"))
+local d = Equation(parse("e^x+1"), parse("y"))
+local e = Equation(parse("z*sin(x/2)"), parse("4"))
+local f = Equation(parse("4"), parse("0"))
+
+
+starttest("equation solving")
+testeq(a:solvefor(parse("x")), Equation(parse("x"), parse("0")), a)
+testeq(b:solvefor(parse("x")), Equation(parse("x"), parse("-1")), b) -- This will need to be fixed once set expressions are woring
+testeq(c:solvefor(parse("x")), Equation(parse("x^x"), parse("3/2*y")), c)
+testeq(c:solvefor(parse("y")), Equation(parse("y"), parse("2/3*x^x")), c)
+testeq(d:solvefor(parse("x")), Equation(parse("x"), parse("ln(y - 1)")), d)
+testeq(e:solvefor(parse("x")), Equation(parse("x"), parse("2*arcsin(4/z)")), e)
+testeq(f:autosimplify(), "false", f) -- Same, with boolean expressions
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/expressions/functions.lua b/macros/luatex/latex/luacas/tex/test/expressions/functions.lua
new file mode 100644
index 0000000000..e2c15096d6
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/expressions/functions.lua
@@ -0,0 +1,17 @@
+local a = FunctionExpression("f",
+ {SymbolExpression("x"),
+ BinaryOperation.MULEXP
+ ({SymbolExpression("x"),
+ Integer(2)})})
+
+local b = BinaryOperation.ADDEXP
+ ({FunctionExpression("g",
+ {SymbolExpression("x")}),
+ FunctionExpression("f",
+ {SymbolExpression("x")}),
+ Integer(4)})
+
+starttest("function expressions")
+testeq(a:autosimplify(), parse("f(x, (2 * x))"), a)
+testeq(b:autosimplify(), parse("(4 + f(x) + g(x))"), b)
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/expressions/logarithms.lua b/macros/luatex/latex/luacas/tex/test/expressions/logarithms.lua
new file mode 100644
index 0000000000..2813a6b8f5
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/expressions/logarithms.lua
@@ -0,0 +1,26 @@
+local a = LN(SymbolExpression("x"))
+local b = LN(BinaryOperation.POWEXP({E, SymbolExpression("x")}))
+local c = BinaryOperation.POWEXP({Integer(2), LOG(Integer(2), SymbolExpression("y"))})
+local d = dparse("e^(-x*ln(x))")
+
+local e = Logarithm(Integer(2), Integer(256))
+local f = Logarithm(Integer(4), Integer(8))
+local g = Logarithm(Integer(1)/Integer(5), Integer((125)))
+local h = Logarithm(Integer(1)/Integer(9), Integer(1)/Integer(243))
+local i = Logarithm(Integer(1)/Integer(25), Integer(3125))
+
+local k = Logarithm(E, Integer(1)/Integer(9))
+
+starttest("logarithms")
+testeq(a, "log(e, x)")
+testeq(a:autosimplify(), "log(e, x)", a)
+testeq(b:autosimplify(), "x", b)
+testeq(c:autosimplify(), "y", c)
+testeq(d:autosimplify(), parse("x^(-x)"), d)
+testeq(e:autosimplify(), parse("8"), e)
+testeq(f:autosimplify(), parse("3/2"), f)
+testeq(g:autosimplify(), parse("-3"), g)
+testeq(h:autosimplify(), parse("5/2"), h)
+testeq(i:autosimplify(), parse("-5/2"), i)
+testeq(k:autosimplify(), parse("-ln(9)"), k)
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/expressions/rationalexponent.lua b/macros/luatex/latex/luacas/tex/test/expressions/rationalexponent.lua
new file mode 100644
index 0000000000..85690f8c66
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/expressions/rationalexponent.lua
@@ -0,0 +1,15 @@
+local a = BinaryOperation.POWEXP({Integer(8), Integer(1) / Integer(2)})
+local b = BinaryOperation.POWEXP({Integer(27), Integer(1) / Integer(3)})
+local c = BinaryOperation.POWEXP({Integer(36), Integer(1) / Integer(2)})
+local d = BinaryOperation.POWEXP({Integer(36264691), Integer(1) / Integer(2)})
+local e = BinaryOperation.POWEXP({Integer(357911), Integer(1) / Integer(2)})
+local f = BinaryOperation.ADDEXP({BinaryOperation.POWEXP({Integer(8), Integer(1) / Integer(2)}), BinaryOperation.POWEXP({Integer(32), Integer(1) / Integer(2)})})
+
+starttest("rational powers")
+testeq(a:autosimplify(), "(2 * (2 ^ 1/2))", a)
+testeq(b:autosimplify(), "3", b)
+testeq(c:autosimplify(), "6", c)
+testeq(d:autosimplify(), "(331 * (331 ^ 1/2))", d)
+testeq(e:autosimplify(), "(71 * (71 ^ 1/2))", e)
+testeq(f:autosimplify(), "(6 * (2 ^ 1/2))", f)
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/expressions/simplify.lua b/macros/luatex/latex/luacas/tex/test/expressions/simplify.lua
new file mode 100644
index 0000000000..ac5582901d
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/expressions/simplify.lua
@@ -0,0 +1,22 @@
+local a = SymbolExpression("x")*(SymbolExpression("y") + SymbolExpression("z"))
+local b = SymbolExpression("x")*(Integer(1)+ SymbolExpression("z"))
+local c = parse("((2*x+1)*(3*x-1)+6)*(6*y-z)")
+local d = parse("(x+1)*(x+2)*(x+3)")
+
+local e = parse("x*y*z+x^2")
+local f = parse("x + 1/x^2")
+local g = parse("e^x - e^x*x^2")
+
+starttest("expression expansion")
+testeq(a:expand(), parse("((x * y) + (x * z))"), a)
+testeq(b:expand(), parse("(x + (x * z))"), b)
+testeq(c:expand(), parse("((30 * y) + (6 * x * y) + (36 * (x ^ 2) * y) + (-5 * z) + (-1 * x * z) + (-6 * (x ^ 2) * z))"), c)
+testeq(d:expand(), parse("(6 + (11 * x) + (6 * (x ^ 2)) + (x ^ 3))"))
+endtest()
+
+starttest("expression factoring beyond monovariate polynomials")
+testeq(e:factor(), parse("(x * (x + (y * z)))"))
+testeq(f:factor(), parse("((x ^ -2) * (1 + x) * (1 + (-1 * x) + (x ^ 2)))"))
+testeq(g:factor(), parse("((e ^ x) * (1 + x) * (1 + (-1 * x)))"))
+
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/expressions/substitute.lua b/macros/luatex/latex/luacas/tex/test/expressions/substitute.lua
new file mode 100644
index 0000000000..4d1f3496bf
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/expressions/substitute.lua
@@ -0,0 +1,10 @@
+local a = parse("3*(x+1)^1/2-6*y+3*z^2")
+local b = parse("sin(e^x - 1) + e^x")
+
+starttest("substitution")
+testeq(a:substitute({[parse("x")] = Integer(3),
+ [parse("y")] = Integer(-1),
+ [parse("z")] = Integer(4)/Integer(3)}):autosimplify(), parse("52/3"))
+
+testeq(b:substitute({[parse("e^x")] = parse("x^e")}), parse("((x ^ e) + sin((-1 + (x ^ e))))"))
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/helper.lua b/macros/luatex/latex/luacas/tex/test/helper.lua
new file mode 100644
index 0000000000..b550967da9
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/helper.lua
@@ -0,0 +1,322 @@
+-- helper functions
+
+
+ function whatis(a)
+ if a == nil then
+ return nil
+ end
+ if a:type() == SymbolExpression then
+ return "Sym"
+ end
+ if a:type() == BinaryOperation then
+ return "BinOp"
+ end
+ if a:type() == FunctionExpression then
+ return "FncExp"
+ end
+ if a:type() == TrigExpression then
+ return "TrigExp"
+ end
+ if a:type() == Integer then
+ return "Int"
+ end
+ if a:type() == Rational then
+ return "Ratl"
+ end
+ if a:type() == DerivativeExpression then
+ return "DervExp"
+ end
+ if a:type() == DiffExpression then
+ return "DiffExp"
+ end
+ if a:type() == IntegralExpression then
+ return "Intgrl"
+ end
+ if a:type() == SqrtExpression then
+ return "Sqrt"
+ end
+ if a:type() == PolynomialRing then
+ return "Poly"
+ end
+ if a:type() == AbsExpression then
+ return "ABS"
+ end
+ if a:type() == Logarithm then
+ return "LOG"
+ end
+ if a:type() == RootExpression then
+ return "RootOf"
+ end
+ if a:type() == Equation then
+ return "="
+ end
+ return "No Clue"
+end
+
+function longwhatis(a)
+ if a == nil then
+ return nil
+ end
+ if a:type() == SymbolExpression then
+ return "SymbolExpression"
+ end
+ if a:type() == BinaryOperation then
+ return "BinaryOperation"
+ end
+ if a:type() == FunctionExpression then
+ return "FunctionExpression"
+ end
+ if a:type() == TrigExpression then
+ return "TrigExpression"
+ end
+ if a:type() == Integer then
+ return "Integer"
+ end
+ if a:type() == Rational then
+ return "Rational"
+ end
+ if a:type() == DerivativeExpression then
+ return "DerivativeExpression"
+ end
+ if a:type() == DiffExpression then
+ return "DiffExpression"
+ end
+ if a:type() == IntegralExpression then
+ return "IntegralExpression"
+ end
+ if a:type() == SqrtExpression then
+ return "SqrtExpression"
+ end
+ if a:type() == PolynomialRing then
+ return "PolynomialRing"
+ end
+ if a:type() == AbsExpression then
+ return "AbsExpression"
+ end
+ if a:type() == Logarithm then
+ return "Logarithm"
+ end
+ if a:type() == RootExpression then
+ return "RootExpression"
+ end
+ if a:type() == Equation then
+ return "Equation"
+ end
+ return "No Clue"
+end
+
+function whatring(a)
+ if a:getring() == Rational.makering() then
+ return "Rational"
+ end
+ if a:getring() == PolynomialRing.makering() then
+ return "PolynomialRing"
+ end
+ if a:getring() == Integer.makering() then
+ return "Integer"
+ end
+ if a:getring() == IntegerModN.makering() then
+ return "IntegerModN"
+ end
+ return "No Clue"
+end
+
+function nameof(sym)
+ if sym == nil then
+ return nil
+ end
+ if sym:type() == BinaryOperation then
+ local binops = {BinaryOperation.ADD,
+ BinaryOperation.MUL,
+ BinaryOperation.SUB,
+ BinaryOperation.DIV,
+ BinaryOperation.POW,
+ BinaryOperation.IDIV,
+ BinaryOperation.MOD}
+ local obslab = {"ADD",
+ "MUL",
+ "SUB",
+ "DIV",
+ "POW",
+ "IDIV",
+ "MOD"}
+ for i,j in pairs(binops) do
+ if sym.operation == j then
+ return obslab[i]
+ end
+ end
+ end
+ if sym:type() == FunctionExpression or sym:type() == TrigExpression then
+ return tostring(sym.name)
+ end
+ if sym:type() == SymbolExpression or sym:type() == Integer then
+ return tostring(sym)
+ end
+ if sym:type() == Rational then
+ return tostring(sym.numerator).."/"..tostring(sym.denominator)
+ end
+ if sym:type() == DerivativeExpression then
+ return "DD"
+ end
+ if sym:type() == DiffExpression then
+ return "diff"
+ end
+ if sym:type() == IntegralExpression then
+ return "$\\mathtt{\\int}$"
+ end
+ if sym:type() == SqrtExpression then
+ return "$\\mathtt{\\sqrt{\\phantom{x}}}$"
+ end
+ if sym:type() == PolynomialRing then
+ return "Poly"
+ end
+ if sym:type() == AbsExpression then
+ return "abs"
+ end
+ if sym:type() == Logarithm then
+ return "log"
+ end
+ if sym:type() == RootExpression then
+ return "RootOf"
+ end
+ if sym:type() == Equation then
+ return "$\\mathtt{=}$"
+ end
+ return "No Clue"
+end
+
+function Expression:getfullsubexpressionsrec()
+ local result = {}
+ for _, expression in ipairs(self:subexpressions()) do
+ result[#result+1] = expression
+ result = JoinArrays(result, expression:getfullsubexpressionsrec())
+ end
+ return result
+end
+
+function Expression:gettheshrub()
+ local string = ""
+ for index, expression in ipairs(self:subexpressions()) do
+ string = string.."child {node [label=-90:{expr["..tostring(index).."]}] {$\\mathtt{"..expression:tolatex().."}$}}"
+ end
+ return string
+end
+
+function Expression:getthetree()
+ local string = ""
+ for _, expression in ipairs(self:subexpressions()) do
+ if expression:isatomic() then
+ string = string.."child {node{"..nameof(expression).."}}"
+ else
+ string = string.."child {node{"..nameof(expression).."}"..expression:getthetree().."}"
+ end
+ end
+ return string
+end
+
+function Expression:gettheforest()
+ local string = ""
+ for _, expression in ipairs(self:subexpressions()) do
+ if expression:isatomic() then
+ string = string.." [ "..nameof(expression).." ] "
+ else
+ string = string.." [ "..nameof(expression)..expression:gettheforest().." ] "
+ end
+ end
+ return string
+end
+
+function Expression:getthefancyshrub()
+ local string = ""
+ if self:type() == DiffExpression then
+ for _, expression in ipairs(self:subexpressions()) do
+ string = string.." [ $\\mathtt{"..expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ] "
+ end
+ string = string.." [ $\\mathtt{\\{"
+ for _,symbol in ipairs(self.symbols) do
+ if next(self.symbols,_) == nil then
+ string = string .. symbol:tolatex().."\\}}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.symbols};} ] "
+ else
+ string = string .. symbol:tolatex() .. ","
+ end
+ end
+ return string
+ end
+ if self:type() == IntegralExpression then
+ string = string .. " [ $\\mathtt{"..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ] "
+ string = string .. "[ $\\mathtt{"..self.symbol:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.symbol};} ]"
+ if self:isdefinite() then
+ string = string .. "[ $\\mathtt{"..self.lower:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.lower};} ] "
+ string = string .. "[ $\\mathtt{"..self.upper:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.upper};} ] "
+ return string
+ end
+ return string
+ end
+ if self:type() == PolynomialRing then
+ string = string .. " [ $\\mathtt{\\{"
+ for index=0, self.degree:asnumber() do
+ string = string .. tostring(self.coefficients[index])
+ if index < self.degree:asnumber() then
+ string = string .. ","
+ end
+ end
+ string = string .. "\\} }$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.coefficients}; \\node[anchor=south west, font=\\ttfamily\\footnotesize,gray] at (.north west) {.ring "..whatring(self).."};} ]"
+ string = string .. " [ $\\mathtt{"..self.symbol.. "}$, tikz+={\\node[anchor=north, font=\\ttfamily\\footnotesize,gray] at (.south) {.symbol};} ]"
+ return string
+ end
+ if self:type() == SqrtExpression then
+ string = string .. " [ $\\mathtt{"..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ]"
+ string = string .. "[ $\\mathtt{"..self.root:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.root};} ]"
+ return string
+ end
+ if self:type() == TrigExpression then
+ string = string .. " [ $\\mathtt{"..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ]"
+ return string
+ end
+ if self:type() == AbsExpression then
+ string = string .. " [ $\\mathtt{"..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ] "
+ return string
+ end
+ if self:type() == Logarithm then
+ string = string .. " [$\\mathtt{" ..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ]"
+ string = string .. " [$\\mathtt{" ..self.base:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.base};} ]"
+ return string
+ end
+ if self:type() == RootExpression then
+ string = string .. "[$\\mathtt{" ..self.expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression};} ]"
+ return string
+ end
+ if self:type() == FunctionExpression then
+ local string1 = ''
+ local string2 = ''
+ local string3 = ''
+ for index=1, #self.variables do
+ string1 = string1 .. tostring(self.expressions[index])
+ if index < #self.variables then
+ string1 = string1 .. ","
+ end
+ string2 = string2 .. tostring(self.variables[index])
+ if index < #self.variables then
+ string2 = string2 .. ","
+ end
+ string3 = string3 .. tostring(self.derivatives[index])
+ if index < #self.variables then
+ string3 = string3 .. ","
+ end
+ end
+ string = string .. "[$\\mathtt{ \\{" .. string1 .. "\\}}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expressions};} ]"
+ string = string .. "[$\\mathtt{ \\{" .. string2 .. "\\}}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.variables};} ]"
+ string = string .. "[$\\mathtt{ \\{" .. string3 .. "\\}}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.derivatives};} ]"
+ return string
+ end
+ if self:type() == Equation then
+ string = string .. " [$\\mathtt{" ..self.lhs:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.lhs};} ]"
+ string = string .. " [$\\mathtt{" ..self.rhs:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.rhs};} ]"
+ return string
+ end
+ for index, expression in ipairs(self:subexpressions()) do
+ string = string.." [ $\\mathtt{"..expression:tolatex().."}$, tikz+={\\node[anchor=north,font=\\ttfamily\\footnotesize,gray] at (.south) {.expression["..index.."]};} ] "
+ end
+ return string
+end
+
diff --git a/macros/luatex/latex/luacas/tex/test/main.lua b/macros/luatex/latex/luacas/tex/test/main.lua
new file mode 100644
index 0000000000..638f7734b5
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/main.lua
@@ -0,0 +1,154 @@
+---@diagnostic disable: lowercase-global
+-- Runs test code from test files.
+
+require("calculus._init")
+require("_lib.pepperfish")
+
+-- Stuff required for the basic parser.
+local constants = {e="E", pi = "PI", ln = "LN", log = "LOG", Integer = "Integer", DD = "DD", int = "INT", abs = "ABS", fact="FACT"}
+
+local function parser(s)
+ if string.find(s, "[0-9]+") then
+ return "Integer(\"" .. s .. "\")"
+ end
+
+ if s.find(s, "[%^%\\%[%]]") then
+ return string.gsub(s, "[^%^%\\%[%]]+", parser)
+ end
+
+ for string, replace in pairs(constants) do
+ if s == string then
+ return replace
+ end
+ end
+
+ return "SymbolExpression(\"" .. s .. "\")"
+end
+
+function parse(input)
+ local parsed = string.gsub(input, "[0-9]+", parser)
+ parsed = string.gsub(parsed, "[A-z']+", parser)
+ local exe, err = load("return " .. parsed)
+ if exe then
+ return exe():autosimplify()
+ else
+ print(err)
+ end
+end
+
+function dparse(input)
+ local parsed = string.gsub(input, "[0-9]+", parser)
+ parsed = string.gsub(parsed, "[A-z']+", parser)
+ local exe, err = load("return " .. parsed)
+ if exe then
+ return exe()
+ else
+ print(err)
+ end
+end
+
+-- Stuff required for test code.
+local tests
+local failures
+local totaltests = 0
+local totalfailures = 0
+function starttest(name)
+ print("Testing " .. name .. "...")
+ print()
+ tests = 0
+ failures = 0
+end
+
+-- Tests two objects for equality, irrespective of order. If the object is a table or expression, the objects may be sorted to ensure the correct order.
+function testeq(actual, expected, initial, sort)
+ if sort and type(actual) == "table" and not actual.type then
+ table.sort(actual, function (a, b)
+ return a:order(b)
+ end)
+ elseif sort and type(actual) == "table" and actual.type and actual:type() == BinaryOperation and actual:iscommutative() then
+ table.sort(actual.expressions, function (a, b)
+ return a:order(b)
+ end)
+ end
+
+ if initial then
+ if ToStringArray(expected) == ToStringArray(actual) then
+ print(ToStringArray(initial) .. " -> " .. ToStringArray(actual))
+ else
+ print(ToStringArray(initial) .. " -> " .. ToStringArray(actual) .. " (Expected: " .. ToStringArray(expected) .. ")")
+ failures = failures + 1
+ end
+ else
+ if ToStringArray(expected) == ToStringArray(actual) then
+ print("Result: " .. ToStringArray(actual))
+ else
+ print("Result: ".. ToStringArray(actual) .. " (Expected: " .. ToStringArray(expected) .. ")")
+ failures = failures + 1
+ end
+ end
+ tests = tests + 1
+end
+
+-- Tests whether converting an element to a different ring produces the expected object in the expected ring
+function testringconvert(expression, toring, expected, expectedring)
+ testeq(expression:inring(toring), expected, expression)
+ testeq(expression:inring(toring):getring(), expectedring)
+end
+
+function endtest()
+ print()
+ print("Finished test without errors.")
+ print()
+ totaltests = totaltests + tests
+ totalfailures = totalfailures + failures
+ if failures == 0 then
+ print("Performed " .. tests .. " tests, all of which passed!")
+ else
+ print("Performed tests, " .. failures .. "/" .. tests .. " failed.")
+ end
+ print("=====================================================================================================================")
+end
+
+function endall()
+ if totalfailures == 0 then
+ print("Performed " .. totaltests .. " tests in total, all of which passed!")
+ else
+ print("Performed tests, " .. totalfailures .. "/" .. totaltests .. " failed.")
+ end
+end
+
+
+-- TODO: Add profiling and error catching options.
+-- Comment out these lines to only run certain test code.
+
+-- profiler = newProfiler()
+-- profiler:start()
+
+require("test.calculus.derivatives")
+require("test.calculus.integrals")
+
+require("test.expressions.autosimplify")
+require("test.expressions.collect")
+require("test.expressions.equations")
+require("test.expressions.simplify")
+require("test.expressions.functions")
+require("test.expressions.logarithms")
+-- require("test.expressions.rationalexponent")
+require("test.expressions.substitute")
+
+require("test.polynomials.polynomial")
+require("test.polynomials.partialfractions")
+require("test.polynomials.polynomialmod")
+require("test.polynomials.roots")
+
+require("test.rings.conversion")
+require("test.rings.modulararithmetic")
+require("test.rings.number")
+
+endall()
+
+-- profiler:stop()
+
+-- local outfile = io.open( "profile.txt", "w+" )
+-- profiler:report( outfile )
+-- outfile:close() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/parser.lua b/macros/luatex/latex/luacas/tex/test/parser.lua
new file mode 100644
index 0000000000..6cb22a1bdb
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/parser.lua
@@ -0,0 +1,323 @@
+-- Rudimentary parser for making the CAS easier to use. Essentially just wraps SymbolExpression() around symbols and Integer() around integers.
+
+
+
+require("calculus._init")
+
+-- Splits a string on a seperator.
+function split(str, sep)
+ local t={}
+ for match in string.gmatch(str, "([^".. sep .."]+)") do
+ t[#t+1] = match
+ end
+ return t
+end
+
+-- Displays an expression. For use in the parser.
+function disp(expression, inline, simple)
+ if type(expression) ~= "table" then
+ tex.print(tostring(expression))
+ elseif expression.autosimplify then
+ if inline then
+ if simple then
+ tex.print('$' .. expression:autosimplify():tolatex() .. '$')
+ else
+ tex.print('$' .. expression:tolatex() .. '$')
+ end
+ else
+ if simple then
+ tex.print('\\[' .. expression:autosimplify():tolatex() .. '\\]')
+ else
+ tex.print('\\[' .. expression:tolatex() .. '\\]')
+ end
+ end
+ else
+ tex.print(tostring(expression))
+ end
+end
+
+-- Displays an expression. For use in the parser.
+function displua(expression)
+ if type(expression) ~= "table" then
+ print(tostring(expression))
+ elseif expression.autosimplify then
+ print(expression:autosimplify():tolatex())
+ else
+ print(tostring(expression))
+ end
+end
+
+function vars(...)
+ for _, string in ipairs(table.pack(...)) do
+ if string ~= "_" then
+ _G[string] = SymbolExpression(string)
+ end
+ end
+end
+
+function clearvars()
+ for index, value in pairs(_G) do
+ if type(value) == "table" and value.type and value:type() == SymbolExpression then
+ _G[index] = nil
+ end
+ end
+end
+
+function range(a, b, step)
+ if not b then
+ b = a
+ a = Integer.one()
+ end
+ step = step or Integer.one()
+ local f =
+ step > Integer.zero() and
+ function(_, lastvalue)
+ local nextvalue = lastvalue + step
+ if nextvalue <= b then return nextvalue end
+ end or
+ step < Integer.zero() and
+ function(_, lastvalue)
+ local nextvalue = lastvalue + step
+ if nextvalue >= b then return nextvalue end
+ end or
+ function(_, lastvalue) return lastvalue end
+ return f, nil, a - step
+ end
+
+function factor(exp,squarefrei)
+ if exp:type() == Integer then
+ return exp:primefactorization()
+ end
+ if exp:type() == PolynomialRing then
+ if not squarefrei then
+ return exp:factor()
+ else
+ if exp.ring == Integer.getring() or Rational.getring() then
+ return exp:squarefreefactorization()
+ end
+ if exp.ring == IntegerModN.getring() then
+ return exp:modularsquarefreefactorization()
+ end
+ return exp:factor()
+ end
+ end
+ return exp:autosimplify():factor()
+end
+
+function expand(exp)
+ return exp:autosimplify():expand()
+end
+
+function simplify(exp)
+ return exp:simplify()
+end
+
+function exp(x)
+ return e^x
+end
+
+function substitute(tbl,expr)
+ return expr:substitute(tbl)
+end
+
+function roots(expression)
+ poly,ispoly = topoly(expression)
+ if ispoly then
+ return poly:roots()
+ end
+ return RootExpression(expression)
+end
+
+function combine(expr)
+ return expr:combine()
+end
+
+function Mod(f,n)
+ if f:type() == Integer then
+ return IntegerModN(f,n)
+ end
+ if f:type() == PolynomialRing and f.ring == Integer.getring() then
+ local coeffs = {}
+ for i=0,f.degree:asnumber() do
+ coeffs[i] = IntegerModN(f.coefficients[i],n)
+ end
+ return PolynomialRing(coeffs,f.symbol,f.degree)
+ end
+end
+
+function Poly(coefficients,symbol,degree)
+ local variable = symbol or 'x'
+ return PolynomialRing:new(coefficients,variable,degree)
+end
+
+function topoly(a)
+ a = a:expand():autosimplify()
+ return a:topolynomial()
+end
+
+function gcd(a,b)
+ if a:type() == Integer and b:type() == Integer then
+ return Integer.gcd(a,b)
+ end
+ if a:type() == PolynomialRing and b:type() == PolynomialRing then
+ return PolynomialRing.gcd(a,b)
+ end
+end
+
+function gcdext(a,b)
+ if a:type() == Integer and b:type() == Integer then
+ return Integer.extendedgcd(a,b)
+ end
+ A, ATF = topoly(a)
+ B, BTF = topoly(b)
+ if ATF and BTF then
+ return PolynomialRing.extendedgcd(A,B)
+ end
+ return nil,nil,nil
+end
+
+function parfrac(f,g,ffactor)
+ local f,check1 = topoly(f)
+ local g,check2 = topoly(g)
+ if check1 and check2 then
+ if f.degree >= g.degree then
+ local q,r
+ q,r = f:divremainder(g)
+ return q + PolynomialRing.partialfractions(r,g,ffactor)
+ else
+ return PolynomialRing.partialfractions(f,g,ffactor)
+ end
+ else
+ return f/g
+ end
+end
+
+function factorial(a)
+ return FactorialExpression(a)
+end
+
+-- Constants for the CAS. We may not want these in Lua itself, but in the latex end the user probably expects them.
+e = E
+pi = PI
+-- sqrt = SQRT
+ln = LN
+log = LOG
+int = INT
+sin = SIN
+cos = COS
+tan = TAN
+csc = CSC
+sec = SEC
+cot = COT
+arcsin = ARCSIN
+arccos = ARCCOS
+arctan = ARCTAN
+arccsc = ARCCSC
+arcsec = ARCSEC
+arccot = ARCCOT
+abs = ABS
+
+function ZTable(t)
+ t = t or {}
+ return setmetatable(t, JoinTables(getmetatable(t),
+ {__index = function (t, k)
+ if type(k) == "table" and k.type and k:type() == Integer then
+ return rawget(t, k:asnumber())
+ else
+ return rawget(t, k)
+ end
+ end,
+ __newindex = function (t, k, v)
+ if type(k) == "table" and k.type and k:type() == Integer then
+ rawset(t, k:asnumber(), v)
+ else
+ rawset(t, k, v)
+ end
+ end}))
+end
+
+function RR(n)
+ if type(n) == "number" then
+ return n
+ end
+
+ if type(n) == "string" then
+ return tonumber(n)
+ end
+
+ if type(n) == "table" and n.asnumber then
+ return n:asnumber()
+ end
+
+ error("Could not convert to a real number.")
+end
+
+function ZZ(n)
+ if type(n) == "table" and n.type and n:type() == Rational then
+ return n.numerator // n.denominator
+ end
+ return Integer(n)
+end
+
+function QQ(n)
+ if type(n) == "table" then
+ return n
+ end
+
+ if type(n) == "number" then
+ n = tostring(n)
+ end
+
+ if type(n) == "string" then
+ local parts = split(n, "%.")
+ if #parts == 1 then
+ return Integer(parts[1])
+ else
+ return Integer(parts[1])..Integer(parts[2])
+ end
+ end
+
+ error("Could not convert to a rational number.")
+end
+
+--- Parses raw input into Lua code and executes it.
+--- @param input string
+function CASparse(input)
+
+ -- First, we replace any occurance of a number with an integer or rational version of itself.
+ local str = string.gsub(input, ".?[0-9]+", function (s)
+ -- Here, we are part of an identifier, so we don't replace anything
+ if string.match(string.sub(s, 1, 1), "[A-Z]") or string.match(string.sub(s, 1, 1), "[a-z]") or string.match(string.sub(s, 1, 1), "_") then
+ return
+ end
+
+ if string.match(string.sub(s, 1, 1), "[0-9]") then
+ return "Integer('" .. s .. "')"
+ end
+
+ return string.sub(s, 1, 1) .. "Integer('" .. string.sub(s, 2, #s) .. "')"
+ end)
+
+ --------------------------
+ -- HERE COMES THE JANK. --
+ --------------------------
+
+ -- Replaces each instance of a decimal with .., so we can use integer metatables to convert it into a rational properly.
+ str = string.gsub(str, "Integer%('[0-9]+'%)%.Integer%('[0-9]+'%)", function (s)
+ local ints = split(s, "%.")
+ return ints[1] .. ".." .. ints[2]
+ end)
+ str = string.gsub(str, ".?%.Integer%('[0-9]+'%)", function (s)
+ if string.sub(s, 1, 2) == ".." then
+ return
+ end
+ return string.sub(s, 1, 1) .. "Integer('0')." .. string.sub(s, 2, #s)
+ end)
+
+ local exe, err = load(str .. "\n return true")
+ if exe then
+ exe()
+ else
+ print(err)
+ end
+end \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/polynomials/partialfractions.lua b/macros/luatex/latex/luacas/tex/test/polynomials/partialfractions.lua
new file mode 100644
index 0000000000..280d4d56f3
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/polynomials/partialfractions.lua
@@ -0,0 +1,12 @@
+local g1 = parse("x^3+4*x^2-x-2"):topolynomial()
+local f1 = parse("x^4-x^2"):topolynomial()
+
+local g2 = parse("2*x^6-4*x^5+5*x^4-3*x^3+x^2+3*x"):topolynomial()
+local f2 = parse("x^7-3*x^6+5*x^5-7*x^4+7*x^3-5*x^2+3*x-1"):topolynomial()
+
+starttest("partial fraction decomposition")
+
+testeq(PolynomialRing.partialfractions(g1, f1):autosimplify(), parse("((2 * (x ^ -2)) + (x ^ -1) + ((-1 + x) ^ -1) + (-1 * ((1 + x) ^ -1)))"))
+testeq(PolynomialRing.partialfractions(g2, f2):autosimplify(), parse("(((-1 + x) ^ -3) + ((-1 + x) ^ -1) + ((1 + (x ^ 2)) ^ -2) + ((1 + x) * ((1 + (x ^ 2)) ^ -1)))"))
+
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/polynomials/polynomial.lua b/macros/luatex/latex/luacas/tex/test/polynomials/polynomial.lua
new file mode 100644
index 0000000000..545538c764
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/polynomials/polynomial.lua
@@ -0,0 +1,153 @@
+
+local a = PolynomialRing({
+ Integer(1),
+ Integer(2),
+ Integer(3),
+ Integer(4),
+ Integer(5)
+}, "x")
+
+local b = PolynomialRing({
+ Integer(1) / Integer(3),
+ Integer(1) / Integer(12),
+ Integer(6) / Integer(3),
+}, "x")
+
+local c = PolynomialRing({
+ Integer(12),
+ Integer(4)
+}, "x")
+
+local h = PolynomialRing({Integer(2), Integer(3), Integer(1)}, "x")
+local i = PolynomialRing({Integer(8), Integer(20), Integer(18), Integer(7), Integer(1)}, "x")
+local j = PolynomialRing({Integer(108), Integer(324), Integer(387), Integer(238), Integer(80), Integer(14), Integer(1)}, "x")
+local k = PolynomialRing({Integer(30), Integer(11), Integer(1)}, "x")
+local l = PolynomialRing({Integer(6), Integer(11), Integer(6), Integer(1)}, "z")
+local m = PolynomialRing({Integer(1), Integer(10), Integer(45), Integer(120), Integer(210), Integer(252), Integer(210), Integer(120), Integer(45), Integer(10), Integer(1)}, "x")
+local n = PolynomialRing({Integer(24), Integer(50), Integer(35), Integer(10), Integer(1), Integer(0), Integer(24), Integer(50), Integer(35), Integer(10), Integer(1)}, "x")
+local o = PolynomialRing({Integer(24), Integer(50), Integer(59), Integer(60), Integer(36), Integer(10), Integer(1)}, "x")
+local p = PolynomialRing({Integer(-110592), Integer(59904), Integer(-5760), Integer(720), Integer(-48), Integer(1)}, "x")
+
+local d = PolynomialRing({Integer(21), Integer(10), Integer(1)}, "x")
+local e = PolynomialRing({Integer(-6), Integer(1), Integer(1)}, "x")
+
+local f = PolynomialRing({Integer(-1), Integer(-2), Integer(15), Integer(36)}, "x")
+local g = PolynomialRing({Integer(1), Integer(7), Integer(15), Integer(9)}, "x")
+
+local q = PolynomialRing({Integer(3), Integer(-9), Integer(27), Integer(-36), Integer(36)}, "z")
+local r = PolynomialRing({Integer(0), Integer(0), Integer(0), Integer(0), Integer(0), Integer(0), Integer(4)}, "x");
+local s = PolynomialRing({Integer(1), Integer(0), Integer(-4), Integer(0), Integer(1)}, "x")
+
+local x = Integer(3)
+local y = Integer(-1) / Integer(6)
+
+local multia = PolynomialRing({Integer(4),
+ Integer(0),
+ PolynomialRing({Integer(0), Integer(0), Integer(-6)}, "y"),
+ PolynomialRing({Integer(1), Integer(3)}, "y")}, "x")
+local multib = PolynomialRing({PolynomialRing({Integer(0), Integer(6)}, "y"),
+ Integer(0),
+ PolynomialRing({Integer(-4), Integer(12)}, "y")}, "x")
+
+starttest("polynomial construction")
+testeq(a, "5x^4+4x^3+3x^2+2x^1+1x^0")
+testeq(a.degree, 4)
+testeq(b, "2x^2+1/12x^1+1/3x^0")
+testeq(b.degree, 2)
+testeq(multia, "(3y^1+1y^0)x^3+(-6y^2+0y^1+0y^0)x^2+(0)x^1+(4)x^0")
+testeq(multia.degree, 3)
+endtest()
+
+starttest("polynomial-expression conversion")
+testeq(a:tocompoundexpression():autosimplify():topolynomial(), a)
+testeq(b:tocompoundexpression():autosimplify():topolynomial(), b)
+testeq(c:tocompoundexpression():autosimplify():topolynomial(), c)
+endtest()
+
+starttest("polynomial arithmetic")
+testeq(a + a, "10x^4+8x^3+6x^2+4x^1+2x^0")
+testeq(a + b, "5x^4+4x^3+5x^2+25/12x^1+4/3x^0")
+testeq(b + a, "5x^4+4x^3+5x^2+25/12x^1+4/3x^0")
+testeq(a - a, "0x^0")
+testeq(a - b, "5x^4+4x^3+1x^2+23/12x^1+2/3x^0")
+testeq(b:multiplyDegree(4), "2x^6+1/12x^5+1/3x^4+0x^3+0x^2+0x^1+0x^0")
+testeq(a:multiplyDegree(12), "5x^16+4x^15+3x^14+2x^13+1x^12+0x^11+0x^10+0x^9+0x^8+0x^7+0x^6+0x^5+0x^4+0x^3+0x^2+0x^1+0x^0")
+testeq(c * c, "16x^2+96x^1+144x^0")
+testeq(a * c, "20x^5+76x^4+60x^3+44x^2+28x^1+12x^0")
+testeq(c * a, "20x^5+76x^4+60x^3+44x^2+28x^1+12x^0")
+testeq(b * c, "8x^3+73/3x^2+7/3x^1+4x^0")
+local qq, rr = a:divremainder(c)
+testeq(qq, "5/4x^3+-11/4x^2+9x^1+-53/2x^0")
+testeq(rr, "319x^0")
+qq, rr = a:divremainder(b)
+testeq(qq, "5/2x^2+91/48x^1+1157/1152x^0")
+testeq(rr, "17755/13824x^1+2299/3456x^0")
+endtest()
+
+starttest("polynomial pseudodivision")
+local pq, pr = a:pseudodivide(c)
+testeq(pq, "320x^3+-704x^2+2304x^1+-6784x^0")
+testeq(pr, "81664x^0")
+
+pq, pr = multia:pseudodivide(multib)
+testeq(pq, "(36y^2+0y^1+-4y^0)x^1+(-72y^3+24y^2+0y^1+0y^0)x^0")
+testeq(pr, "(-216y^3+0y^2+24y^1+0y^0)x^1+(432y^4+-144y^3+576y^2+-384y^1+64y^0)x^0")
+endtest()
+
+
+starttest("combined polynomial/coefficient operations")
+testeq(a + x, "5x^4+4x^3+3x^2+2x^1+4x^0")
+testeq(x + a, "5x^4+4x^3+3x^2+2x^1+4x^0")
+testeq(b - y, "2x^2+1/12x^1+1/2x^0")
+testeq(a * x, "15x^4+12x^3+9x^2+6x^1+3x^0")
+testeq(x * a, "15x^4+12x^3+9x^2+6x^1+3x^0")
+endtest()
+
+starttest("polynomial formal derivatives")
+testeq(a:derivative(), "20x^3+12x^2+6x^1+2x^0")
+testeq(b:derivative(), "4x^1+1/12x^0")
+testeq(c:derivative():derivative(), "0x^0")
+endtest()
+
+
+starttest("polynomial gcd...")
+testeq(PolynomialRing.gcd(d, e), "1x^1+3x^0")
+testeq(PolynomialRing.gcd(b, c), "1x^0")
+testeq(PolynomialRing.gcd(f, g), "1x^2+2/3x^1+1/9x^0")
+endtest()
+
+starttest("square-free factorization")
+testeq(h:squarefreefactorization():autosimplify(), parse("(2 + (3 * x) + (x ^ 2))"), h)
+testeq(i:squarefreefactorization():autosimplify(), parse("((1 + x) * ((2 + x) ^ 3))"), i)
+testeq((Integer(2)*i):squarefreefactorization():autosimplify(), parse("(((2 + x) ^ 3) * (2 + (2 * x)))"), (Integer(2)*i), true)
+testeq(j:squarefreefactorization():autosimplify(), parse("((1 + x) * ((2 + x) ^ 2) * ((3 + x) ^ 3))"), j)
+testeq(o:squarefreefactorization():autosimplify(), parse("(24 + (50 * x) + (59 * (x ^ 2)) + (60 * (x ^ 3)) + (36 * (x ^ 4)) + (10 * (x ^ 5)) + (x ^ 6))"), o)
+endtest()
+
+starttest("polynomial factorization")
+testeq(c:factor(), BinaryOperation.MULEXP({Integer(4), BinaryOperation.POWEXP({PolynomialRing({Integer(3), Integer(1)}, "x"), Integer(1)})}), c)
+testeq(h:factor():autosimplify(), parse("((1 + x) * (2 + x))"), h)
+testeq(k:factor():autosimplify(), parse("((5 + x) * (6 + x))"), k)
+testeq(j:factor():autosimplify(), parse("((1 + x) * ((2 + x) ^ 2) * ((3 + x) ^ 3))"), j)
+testeq(p:factor():autosimplify(), parse("((-24 + x) * (96 + (x ^ 2)) * (48 + (-24 * x) + (x ^ 2)))"), p)
+testeq(l:factor():autosimplify(), parse("((1 + z) * (2 + z) * (3 + z))"), l)
+testeq(m:factor():autosimplify(), parse("((1 + x) ^ 10)"), m)
+testeq(b:factor(), BinaryOperation.MULEXP({Integer(1)/Integer(12), BinaryOperation.POWEXP({PolynomialRing({Integer(4), Integer(1), Integer(24)}, "x"), Integer(1)})}), b)
+testeq(o:factor():autosimplify(), parse("((1 + x) * (2 + x) * (3 + x) * (4 + x) * (1 + (x ^ 2)))"), o)
+testeq(n:factor():autosimplify(), parse("((1 + x) * (2 + x) * (3 + x) * (4 + x) * (1 + (x ^ 2)) * (1 + (-1 * (x ^ 2)) + (x ^ 4)))"), n)
+endtest()
+
+starttest("polynomial decomposition")
+testeq(c:decompose(), "{4x^1+12x^0}", c, true)
+testeq(h:decompose(), "{1x^2+3x^1+2x^0}", h, true)
+testeq(k:decompose(), "{1x^2+11x^1+30x^0}", k, true)
+testeq(j:decompose(), "{1x^6+14x^5+80x^4+238x^3+387x^2+324x^1+108x^0}", j, true)
+testeq(l:decompose(), "{1z^3+6z^2+11z^1+6z^0}", l, true)
+testeq(m:decompose(), "{1x^5+5x^4+10x^3+10x^2+5x^1+1x^0, 1x^2+2x^1+0x^0}", m, true)
+testeq(b:decompose(), "{2x^2+1/12x^1+1/3x^0}", b, true)
+testeq(o:decompose(), "{1x^6+10x^5+36x^4+60x^3+59x^2+50x^1+24x^0}", o, true)
+testeq(n:decompose(), "{1x^10+10x^9+35x^8+50x^7+24x^6+0x^5+1x^4+10x^3+35x^2+50x^1+24x^0}", n, true)
+testeq(q:decompose(), "{36z^2+18z^1+3z^0, 1z^2+-1/2z^1+0z^0}", q, true)
+testeq(r:decompose(),"{4x^3+0x^2+0x^1+0x^0, 1x^2+0x^1+0x^0}", r, true)
+testeq(s:decompose(), "{1x^2+4x^1+1x^0, 1x^2+0x^1+-4x^0}", s, true)
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/polynomials/polynomialmod.lua b/macros/luatex/latex/luacas/tex/test/polynomials/polynomialmod.lua
new file mode 100644
index 0000000000..1906ad2d72
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/polynomials/polynomialmod.lua
@@ -0,0 +1,76 @@
+local a = PolynomialRing({IntegerModN(Integer(1), Integer(11)),
+ IntegerModN(Integer(6), Integer(11)),
+ IntegerModN(Integer(1), Integer(11)),
+ IntegerModN(Integer(9), Integer(11)),
+ IntegerModN(Integer(1), Integer(11))}, "y")
+
+local b = PolynomialRing({IntegerModN(Integer(7), Integer(11)),
+ IntegerModN(Integer(7), Integer(11)),
+ IntegerModN(Integer(6), Integer(11)),
+ IntegerModN(Integer(2), Integer(11)),
+ IntegerModN(Integer(1), Integer(11))}, "y")
+
+local q = PolynomialRing({IntegerModN(Integer(2), Integer(13)),
+ IntegerModN(Integer(6), Integer(13)),
+ IntegerModN(Integer(4), Integer(13))}, "z")
+
+local p = PolynomialRing({IntegerModN(Integer(4), Integer(13)),
+ IntegerModN(Integer(11), Integer(13)),
+ IntegerModN(Integer(1), Integer(13)),
+ IntegerModN(Integer(12), Integer(13)),
+ IntegerModN(Integer(1), Integer(13))}, "x")
+
+local r = PolynomialRing({IntegerModN(Integer(1), Integer(3)),
+ IntegerModN(Integer(0), Integer(3)),
+ IntegerModN(Integer(0), Integer(3)),
+ IntegerModN(Integer(2), Integer(3)),
+ IntegerModN(Integer(0), Integer(3)),
+ IntegerModN(Integer(0), Integer(3)),
+ IntegerModN(Integer(1), Integer(3))}, "x")
+
+local s = PolynomialRing({IntegerModN(Integer(1), Integer(5)),
+ IntegerModN(Integer(1), Integer(5)),
+ IntegerModN(Integer(1), Integer(5)),
+ IntegerModN(Integer(1), Integer(5)),
+ IntegerModN(Integer(1), Integer(5))}, "x")
+
+local t = PolynomialRing({IntegerModN(Integer(1), Integer(7))}, "x"):multiplyDegree(7) - PolynomialRing({IntegerModN(Integer(1), Integer(7))}, "x"):multiplyDegree(1)
+
+local u = PolynomialRing({IntegerModN(Integer(1), Integer(13)),
+ IntegerModN(Integer(5), Integer(13)),
+ IntegerModN(Integer(6), Integer(13)),
+ IntegerModN(Integer(5), Integer(13)),
+ IntegerModN(Integer(1), Integer(13))}, "x")
+
+local v = PolynomialRing({IntegerModN(Integer(24), Integer(7)),
+ IntegerModN(Integer(50), Integer(7)),
+ IntegerModN(Integer(59), Integer(7)),
+ IntegerModN(Integer(60), Integer(7)),
+ IntegerModN(Integer(36), Integer(7)),
+ IntegerModN(Integer(10), Integer(7)),
+ IntegerModN(Integer(1), Integer(7))}, "z")
+
+starttest("modular polynomial operations")
+testeq(q*q, "3z^4+9z^3+0z^2+11z^1+4z^0")
+testeq(PolynomialRing.gcd(a, b), "1y^0")
+local Q, R, S = PolynomialRing.extendedgcd(a, b)
+testeq(Q, "1y^0")
+testeq(R, "4y^3+5y^2+1y^1+3y^0")
+testeq(S, "7y^3+0y^2+7y^1+6y^0")
+endtest()
+
+starttest("modular square free factoring")
+testeq(p:squarefreefactorization(), Integer(1) * BinaryOperation.POWEXP({(PolynomialRing({Integer(2), Integer(6), Integer(1)}, SymbolExpression("x"))), Integer(2)}))
+testeq(r:squarefreefactorization(), Integer(1) * BinaryOperation.POWEXP({(PolynomialRing({Integer(1), Integer(0), Integer(0), Integer(1)}, SymbolExpression("x"))), Integer(2)}))
+testeq(q:squarefreefactorization(), Integer(4) * BinaryOperation.POWEXP({(PolynomialRing({Integer(7), Integer(8), Integer(1)}, SymbolExpression("z"))), Integer(1)}))
+testeq(s:squarefreefactorization(), Integer(1) * BinaryOperation.POWEXP({(PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("x"))), Integer(4)}))
+endtest()
+
+starttest("modular polynomial factoring")
+testeq(q:factor(), BinaryOperation.MULEXP({Integer(4), BinaryOperation.POWEXP({PolynomialRing({Integer(7), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(1), Integer(1)}, SymbolExpression("z")), Integer(1)})}), q)
+testeq(p:factor(), Integer(1) * BinaryOperation.POWEXP({PolynomialRing({Integer(2), Integer(6), Integer(1)}, SymbolExpression("x")), Integer(2)}), p)
+testeq(r:factor(), Integer(1) * BinaryOperation.POWEXP({(PolynomialRing({Integer(1), Integer(0), Integer(0), Integer(1)}, SymbolExpression("x"))), Integer(2)}), r)
+testeq(t:factor(), BinaryOperation.MULEXP({Integer(1), BinaryOperation.POWEXP({PolynomialRing({Integer(0), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(6), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(5), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(3), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(2), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(1), Integer(1)}, SymbolExpression("x")), Integer(1)})}), t)
+testeq(u:factor(), BinaryOperation.MULEXP({Integer(1), BinaryOperation.POWEXP({PolynomialRing({Integer(11), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(10), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(6), Integer(1)}, SymbolExpression("x")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("x")), Integer(1)})}), u)
+testeq(v:factor(), BinaryOperation.MULEXP({Integer(1), BinaryOperation.POWEXP({PolynomialRing({Integer(1), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(2), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(1), Integer(0), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("z")), Integer(1)}), BinaryOperation.POWEXP({PolynomialRing({Integer(3), Integer(1)}, SymbolExpression("z")), Integer(1)})}), v)
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/polynomials/roots.lua b/macros/luatex/latex/luacas/tex/test/polynomials/roots.lua
new file mode 100644
index 0000000000..01975ab1ec
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/polynomials/roots.lua
@@ -0,0 +1,43 @@
+local a = PolynomialRing({Integer(1), Integer(2), Integer(3), Integer(4), Integer(5)}, "x")
+local b = PolynomialRing({Integer(1) / Integer(3), Integer(1) / Integer(12), Integer(6) / Integer(3)}, "x")
+local c = PolynomialRing({Integer(12), Integer(4)}, "x")
+local d = PolynomialRing({Integer(21), Integer(10), Integer(1)}, "x")
+local e = PolynomialRing({Integer(-6), Integer(1), Integer(1)}, "x")
+local f = PolynomialRing({Integer(-1), Integer(-2), Integer(15), Integer(36)}, "x")
+local g = PolynomialRing({Integer(1), Integer(7), Integer(15), Integer(9)}, "x")
+local h = PolynomialRing({Integer(2), Integer(3), Integer(1)}, "x")
+local i = PolynomialRing({Integer(8), Integer(20), Integer(18), Integer(7), Integer(1)}, "x")
+local j = PolynomialRing({Integer(108), Integer(324), Integer(387), Integer(238), Integer(80), Integer(14), Integer(1)}, "x")
+local k = PolynomialRing({Integer(30), Integer(11), Integer(1)}, "x")
+local l = PolynomialRing({Integer(6), Integer(11), Integer(6), Integer(1)}, "z")
+local m = PolynomialRing({Integer(1), Integer(10), Integer(45), Integer(120), Integer(210), Integer(252), Integer(210), Integer(120), Integer(45), Integer(10), Integer(1)}, "x")
+local n = PolynomialRing({Integer(24), Integer(50), Integer(35), Integer(10), Integer(1), Integer(0), Integer(24), Integer(50), Integer(35), Integer(10), Integer(1)}, "x")
+local o = PolynomialRing({Integer(24), Integer(50), Integer(59), Integer(60), Integer(36), Integer(10), Integer(1)}, "x")
+local p = PolynomialRing({Integer(-110592), Integer(59904), Integer(-5760), Integer(720), Integer(-48), Integer(1)}, "x")
+local q = PolynomialRing({Integer(3), Integer(-9), Integer(27), Integer(-36), Integer(36)}, "z")
+local r = PolynomialRing({Integer(0), Integer(0), Integer(0), Integer(0), Integer(0), Integer(0), Integer(4)}, "x")
+local s = PolynomialRing({Integer(1), Integer(0), Integer(-4), Integer(0), Integer(1)}, "x")
+local t = PolynomialRing({Integer(1), Integer(-1), Integer(1), Integer(1)}, "t")
+
+starttest("polynomial root-finding")
+testeq(a:roots(), "{Root Of: (5x^4+4x^3+3x^2+2x^1+1x^0)}", a, true)
+testeq(b:roots(), "{-1/48 + (-1/48 * (383 ^ (1/2)) * i), -1/48 + (1/48 * (383 ^ (1/2)) * i)}", b, true)
+testeq(c:roots(), "{-3}", c, true)
+testeq(d:roots(), "{-7, -3}", d, true)
+testeq(e:roots(), "{-3, 2}", e, true)
+testeq(f:roots(), "{-1/3, 1/4}", f, true)
+testeq(g:roots(), "{-1, -1/3}", g, true)
+testeq(h:roots(), "{-2, -1}", h, true)
+testeq(i:roots(), "{-2, -1}", i, true)
+testeq(j:roots(), "{-3, -2, -1}", j, true)
+testeq(k:roots(), "{-6, -5}", k, true)
+testeq(l:roots(), "{-3, -2, -1}", l, true)
+testeq(m:roots(), "{-1}", m, true)
+testeq(n:roots(), "{-4, -3, -2, -1, i, -1 * i, -1/2 * ((2 + (-2 * (3 ^ (1/2)) * i)) ^ (1/2)), 1/2 * ((2 + (-2 * (3 ^ (1/2)) * i)) ^ (1/2)), -1/2 * ((2 + (2 * (3 ^ (1/2)) * i)) ^ (1/2)), 1/2 * ((2 + (2 * (3 ^ (1/2)) * i)) ^ (1/2))}", n, true)
+testeq(o:roots(), "{-4, -3, -2, -1, i, -1 * i}", o, true)
+testeq(p:roots(), "{24, 12 + (-4 * (6 ^ (1/2))), 12 + (4 * (6 ^ (1/2))), -4 * (6 ^ (1/2)) * i, 4 * (6 ^ (1/2)) * i}", p, true)
+testeq(q:roots(), "{1/4 + (-1/2 * ((-3/4 + (-1/3 * (3 ^ (1/2)) * i)) ^ (1/2))), 1/4 + (1/2 * ((-3/4 + (-1/3 * (3 ^ (1/2)) * i)) ^ (1/2))), 1/4 + (-1/2 * ((-3/4 + (1/3 * (3 ^ (1/2)) * i)) ^ (1/2))), 1/4 + (1/2 * ((-3/4 + (1/3 * (3 ^ (1/2)) * i)) ^ (1/2)))}", q, true)
+testeq(r:roots(), "{0}", r, true)
+testeq(s:roots(), "{-1/2 * ((8 + (-4 * (3 ^ (1/2)))) ^ (1/2)), 1/2 * ((8 + (-4 * (3 ^ (1/2)))) ^ (1/2)), -1/2 * ((8 + (4 * (3 ^ (1/2)))) ^ (1/2)), 1/2 * ((8 + (4 * (3 ^ (1/2)))) ^ (1/2))}", s, true)
+testeq(t:roots(), "{-1/3 + (-4/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (-1/3))) + (-1/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (1/3))), -1/3 + (-4/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (-1/3)) * ((-1/2 + (1/2 * (3 ^ (1/2)) * i)) ^ -1)) + (-1/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (1/3)) * (-1/2 + (1/2 * (3 ^ (1/2)) * i))), -1/3 + (-4/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (-1/3)) * ((-1/2 + (1/2 * (3 ^ (1/2)) * i)) ^ -2)) + (-1/3 * ((19 + (3 * (33 ^ (1/2)))) ^ (1/3)) * ((-1/2 + (1/2 * (3 ^ (1/2)) * i)) ^ 2))}", t, true)
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/rings/conversion.lua b/macros/luatex/latex/luacas/tex/test/rings/conversion.lua
new file mode 100644
index 0000000000..b9eb523c7a
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/rings/conversion.lua
@@ -0,0 +1,273 @@
+local a = Integer(12)
+local b = Integer(3) / Integer(2)
+local c = IntegerModN(Integer(4), Integer(7))
+local d = IntegerModN(Integer(8), Integer(14))
+local e = PolynomialRing({Integer(6), Integer(0), Integer(3)}, SymbolExpression("x"))
+local f = PolynomialRing({Integer(4)/Integer(5), Integer(12)}, SymbolExpression("x"))
+local g = PolynomialRing({
+ PolynomialRing({
+ PolynomialRing({Integer(-4), Integer(12), Integer(1)}, SymbolExpression("x")),
+ PolynomialRing({Integer(0)}, SymbolExpression("x")),
+ PolynomialRing({Integer(8)}, SymbolExpression("x"))
+ },
+ SymbolExpression("y")),
+ PolynomialRing({
+ PolynomialRing({Integer(8), Integer(7), Integer(6), Integer(5)}, SymbolExpression("x")),
+ PolynomialRing({Integer(-1)}, SymbolExpression("x")),
+ },
+ SymbolExpression("y")),
+ PolynomialRing({
+ PolynomialRing({Integer(0)}, SymbolExpression("x")),
+ PolynomialRing({Integer(2), Integer(8), Integer(1)}, SymbolExpression("x")),
+ PolynomialRing({Integer(-16), Integer(4)}, SymbolExpression("x")),
+ PolynomialRing({Integer(1)}, SymbolExpression("x"))
+ },
+ SymbolExpression("y")),
+ PolynomialRing({
+ PolynomialRing({Integer(1)}, SymbolExpression("x"))
+ },
+ SymbolExpression("y"))
+ }, SymbolExpression("z"))
+local h = PolynomialRing({
+ PolynomialRing({
+ PolynomialRing({Integer(-4), Integer(4)/Integer(5), Integer(1)}, SymbolExpression("z")),
+ PolynomialRing({Integer(0)}, SymbolExpression("z")),
+ PolynomialRing({Integer(8)}, SymbolExpression("z"))
+ },
+ SymbolExpression("y")),
+ PolynomialRing({
+ PolynomialRing({Integer(8), Integer(7), Integer(6), Integer(5)}, SymbolExpression("z")),
+ PolynomialRing({Integer(-1)}, SymbolExpression("z")),
+ },
+ SymbolExpression("y")),
+ PolynomialRing({
+ PolynomialRing({Integer(0)}, SymbolExpression("z")),
+ PolynomialRing({Integer(2), Integer(8), Integer(1)}, SymbolExpression("z")),
+ PolynomialRing({Integer(-16), Integer(1)/Integer(9)}, SymbolExpression("z")),
+ PolynomialRing({Integer(1)}, SymbolExpression("z"))
+ },
+ SymbolExpression("y")),
+ PolynomialRing({
+ PolynomialRing({Integer(1)}, SymbolExpression("z"))
+ },
+ SymbolExpression("y"))
+ }, SymbolExpression("x"))
+local i = Rational(PolynomialRing({-Integer(2), Integer(1)}, SymbolExpression("x")),
+ PolynomialRing({Integer(3), Integer(3), Integer(1)}, SymbolExpression("x")))
+local j = Rational(PolynomialRing({-Integer(2), Integer(1)}, SymbolExpression("x")),
+ PolynomialRing({Integer(3)/Integer(4), Integer(3)/Integer(8), Integer(1)}, SymbolExpression("x")))
+local k = PolynomialRing({IntegerModN(Integer(0), Integer(5)),
+ IntegerModN(Integer(1), Integer(5)),
+ IntegerModN(Integer(3), Integer(5)),
+ IntegerModN(Integer(1), Integer(5))}, SymbolExpression("x"))
+
+local l = PolynomialRing({Rational(
+ PolynomialRing({Integer(4), Integer(1)}, SymbolExpression("x")),
+ PolynomialRing({Integer(0), Integer(4)}, SymbolExpression("x"))
+ ),
+ Rational(
+ PolynomialRing({Integer(3)/Integer(2)}, SymbolExpression("x")),
+ PolynomialRing({Integer(6), Integer(1)/Integer(2), Integer(8), Integer(1)}, SymbolExpression("x"))
+ ),
+ Rational(
+ PolynomialRing({Integer(6), Integer(6), Integer(4)}, SymbolExpression("x")),
+ PolynomialRing({Integer(3), Integer(1)}, SymbolExpression("x"))
+ ),
+ Rational(
+ PolynomialRing({Integer(7)/Integer(6)}, SymbolExpression("x")),
+ PolynomialRing({Integer(2), Integer(1)}, SymbolExpression("x"))
+ )
+ }, SymbolExpression("y"))
+
+local aring = a:getring() -- ZZ
+local bring = b:getring() -- QQ
+local cring = c:getring() -- ZZ_7
+local dring = d:getring() -- ZZ_14
+local ering = e:getring() -- ZZ[x]
+local fring = f:getring() -- QQ[x]
+local gring = g:getring() -- ZZ[x][y][z]
+local hring = h:getring() -- QQ[z][y][x]
+local iring = i:getring() -- ZZ(x)
+local jring = j:getring() -- QQ(x)
+local kring = k:getring() -- ZZ_5[x]
+local lring = l:getring() -- QQ(x)[y]
+
+starttest("ring construction")
+testeq(aring, "ZZ")
+testeq(bring, "QQ")
+testeq(cring, "Z/Z7")
+testeq(dring, "Z/Z14")
+testeq(ering, "ZZ[x]")
+testeq(fring, "QQ[x]")
+testeq(gring, "ZZ[x][y][z]")
+testeq(hring, "QQ[z][y][x]")
+testeq(iring, "ZZ(x)")
+testeq(jring, "QQ(x)")
+testeq(kring, "Z/Z5[x]")
+testeq(lring, "QQ(x)[y]")
+endtest()
+
+starttest("ring conversion")
+
+-- Commented-out tests denote elements whos rings are not subrings of the ring that is being converted to
+
+testringconvert(a, aring, "12", "ZZ")
+testringconvert(a, bring, "12/1", "QQ")
+testringconvert(a, cring, "5", "Z/Z7")
+testringconvert(a, dring, "12", "Z/Z14")
+testringconvert(a, ering, "12x^0", "ZZ[x]")
+testringconvert(a, fring, "12/1x^0", "QQ[x]")
+testringconvert(a, gring, "((12x^0)y^0)z^0", "ZZ[x][y][z]")
+testringconvert(a, hring, "((12/1z^0)y^0)x^0", "QQ[z][y][x]")
+testringconvert(a, iring, "(12x^0)/(1x^0)", "ZZ(x)")
+testringconvert(a, jring, "(12x^0)/(1x^0)", "ZZ(x)")
+testringconvert(a, kring, "2x^0", "Z/Z5[x]")
+testringconvert(a, lring, "((12x^0)/(1x^0))y^0", "ZZ(x)[y]")
+
+-- testringconvert(b, aring, "3/2", "ZZ")
+testringconvert(b, bring, "3/2", "QQ")
+-- testringconvert(b, cring, "3/2", "Z/Z7")
+-- testringconvert(b, dring, "3/2", "Z/Z14")
+-- testringconvert(b, ering, "3/2", "ZZ[x]")
+testringconvert(b, fring, "3/2x^0", "QQ[x]")
+-- testringconvert(b, gring, "3/2", "ZZ[x][y][z]")
+testringconvert(b, hring, "((3/2z^0)y^0)x^0", "QQ[z][y][x]")
+-- testringconvert(b, iring, "3/2", "ZZ(x)")
+testringconvert(b, jring, "(3/2x^0)/(1x^0)", "QQ(x)")
+-- testringconvert(b, kring, "3/2", "Z/Z5[x]")
+testringconvert(b, lring, "((3/2x^0)/(1x^0))y^0", "QQ(x)[y]")
+
+testringconvert(c, aring, "4", "ZZ")
+-- testringconvert(c, bring, "4", "QQ")
+testringconvert(c, cring, "4", "Z/Z7")
+testringconvert(c, dring, "4", "Z/Z14")
+testringconvert(c, ering, "4x^0", "ZZ[x]")
+-- testringconvert(c, fring, "4x", "QQ[x]")
+testringconvert(c, gring, "((4x^0)y^0)z^0", "ZZ[x][y][z]")
+-- testringconvert(c, hring, "4", "QQ[z][y][x]")
+testringconvert(c, iring, "(4x^0)/(1x^0)", "ZZ(x)")
+-- testringconvert(c, jring, "12/1x^0/1/1x^0", "QQ(x)")
+testringconvert(c, kring, "4x^0", "Z/Z5[x]")
+-- testringconvert(c, lring, "4", "QQ(x)[y]")
+
+testringconvert(d, aring, "8", "ZZ")
+-- testringconvert(d, bring, "8", "QQ")
+testringconvert(d, cring, "1", "Z/Z7")
+testringconvert(d, dring, "8", "Z/Z14")
+testringconvert(d, ering, "8x^0", "ZZ[x]")
+-- testringconvert(d, fring, "8", "QQ[x]")
+testringconvert(d, gring, "((8x^0)y^0)z^0", "ZZ[x][y][z]")
+-- testringconvert(d, hring, "8", "QQ[z][y][x]")
+testringconvert(d, iring, "(8x^0)/(1x^0)", "ZZ(x)")
+-- testringconvert(d, jring, "8", "QQ(x)")
+testringconvert(d, kring, "3x^0", "Z/Z5[x]")
+-- testringconvert(d, lring, "8", "QQ(x)[y]")
+
+-- testringconvert(e, aring, "3x^2+0x^1+6x^0", "ZZ")
+-- testringconvert(e, bring, "3x^2+0x^1+6x^0", "QQ")
+-- testringconvert(e, cring, "3x^2+0x^1+6x^0", "Z/Z7")
+-- testringconvert(e, dring, "3x^2+0x^1+6x^0", "Z/Z14")
+testringconvert(e, ering, "3x^2+0x^1+6x^0", "ZZ[x]")
+testringconvert(e, fring, "3/1x^2+0/1x^1+6/1x^0", "QQ[x]")
+testringconvert(e, gring, "((3x^2+0x^1+6x^0)y^0)z^0", "ZZ[x][y][z]")
+testringconvert(e, hring, "((3/1z^0)y^0)x^2+((0/1z^0)y^0)x^1+((6/1z^0)y^0)x^0", "QQ[z][y][x]")
+testringconvert(e, iring, "(3x^2+0x^1+6x^0)/(1x^0)", "ZZ(x)")
+testringconvert(e, jring, "(3x^2+0x^1+6x^0)/(1x^0)", "ZZ(x)")
+testringconvert(e, kring, "3x^2+0x^1+1x^0", "Z/Z5[x]")
+testringconvert(e, lring, "((3x^2+0x^1+6x^0)/(1x^0))y^0", "ZZ(x)[y]")
+
+-- testringconvert(f, aring, "12x^1+4/5x^0", "ZZ")
+-- testringconvert(f, bring, "12x^1+4/5x^0", "QQ")
+-- testringconvert(f, cring, "12x^1+4/5x^0", "Z/Z7")
+-- testringconvert(f, dring, "12x^1+4/5x^0", "Z/Z14")
+-- testringconvert(f, ering, "12x^1+4/5x^0", "ZZ[x]")
+testringconvert(f, fring, "12x^1+4/5x^0", "QQ[x]")
+-- testringconvert(f, gring, "12x^1+4/5x^0", "ZZ[x][y][z]")
+testringconvert(f, hring, "((12/1z^0)y^0)x^1+((4/5z^0)y^0)x^0", "QQ[z][y][x]")
+-- testringconvert(f, iring, "12x^1+4/5x^0", "ZZ(x)")
+testringconvert(f, jring, "(12x^1+4/5x^0)/(1x^0)", "QQ(x)")
+-- testringconvert(f, kring, "12x^1+4/5x^0", "Z/Z5[x]")
+testringconvert(f, lring, "((12x^1+4/5x^0)/(1x^0))y^0", "QQ(x)[y]")
+
+-- testringconvert(g, aring, "", "ZZ")
+-- testringconvert(g, bring, "", "QQ")
+-- testringconvert(g, cring, "", "Z/Z7")
+-- testringconvert(g, dring, "", "Z/Z14")
+-- testringconvert(g, ering, "", "ZZ[x]")
+-- testringconvert(g, fring, "", "QQ[x]")
+testringconvert(g, gring, "((1x^0)y^0)z^3+((1x^0)y^3+(4x^1+-16x^0)y^2+(1x^2+8x^1+2x^0)y^1+(0x^0)y^0)z^2+((-1x^0)y^1+(5x^3+6x^2+7x^1+8x^0)y^0)z^1+((8x^0)y^2+(0x^0)y^1+(1x^2+12x^1+-4x^0)y^0)z^0", "ZZ[x][y][z]")
+-- testringconvert(g, hring, "", "QQ[z][y][x]")
+-- testringconvert(g, iring, "", "ZZ(x)")
+-- testringconvert(g, jring, "", "QQ(x)")
+-- testringconvert(g, kring, "", "Z/Z5[x]")
+-- testringconvert(g, lring, "", "QQ(x)[y]")
+
+-- testringconvert(h, aring, "", "ZZ")
+-- testringconvert(h, bring, "", "QQ")
+-- testringconvert(h, cring, "", "Z/Z7")
+-- testringconvert(h, dring, "", "Z/Z14")
+-- testringconvert(h, ering, "", "ZZ[x]")
+-- testringconvert(h, fring, "", "QQ[x]")
+-- testringconvert(h, gring, "", "ZZ[x][y][z]")
+testringconvert(h, hring, "((1z^0)y^0)x^3+((1z^0)y^3+(1/9z^1+-16z^0)y^2+(1z^2+8z^1+2z^0)y^1+(0z^0)y^0)x^2+((-1z^0)y^1+(5z^3+6z^2+7z^1+8z^0)y^0)x^1+((8z^0)y^2+(0z^0)y^1+(1z^2+4/5z^1+-4z^0)y^0)x^0", "QQ[z][y][x]")
+-- testringconvert(h, iring, "", "ZZ(x)")
+-- testringconvert(h, jring, "", "QQ(x)")
+-- testringconvert(h, kring, "", "Z/Z5[x]")
+-- testringconvert(h, lring, "", "QQ(x)[y]")
+
+-- testringconvert(i, aring, "", "ZZ")
+-- testringconvert(i, bring, "", "QQ")
+-- testringconvert(i, cring, "", "Z/Z7")
+-- testringconvert(i, dring, "", "Z/Z14")
+-- testringconvert(i, ering, "", "ZZ[x]")
+-- testringconvert(i, fring, "", "QQ[x]")
+-- testringconvert(i, gring, "", "ZZ[x][y][z]")
+-- testringconvert(i, hring, "", "QQ[z][y][x]")
+testringconvert(i, iring, "(1x^1+-2x^0)/(1x^2+3x^1+3x^0)", "ZZ(x)")
+testringconvert(i, jring, "(1x^1+-2x^0)/(1x^2+3x^1+3x^0)", "ZZ(x)")
+-- testringconvert(i, kring, "", "Z/Z5[x]")
+testringconvert(i, lring, "((1x^1+-2x^0)/(1x^2+3x^1+3x^0))y^0", "ZZ(x)[y]")
+
+-- testringconvert(j, aring, "", "ZZ")
+-- testringconvert(j, bring, "", "QQ")
+-- testringconvert(j, cring, "", "Z/Z7")
+-- testringconvert(j, dring, "", "Z/Z14")
+-- testringconvert(j, ering, "", "ZZ[x]")
+-- testringconvert(j, fring, "", "QQ[x]")
+-- testringconvert(j, gring, "", "ZZ[x][y][z]")
+-- testringconvert(j, hring, "", "QQ[z][y][x]")
+-- testringconvert(j, iring, "", "ZZ(x)")
+testringconvert(j, jring, "(1x^1+-2x^0)/(1x^2+3/8x^1+3/4x^0)", "QQ(x)")
+-- testringconvert(j, kring, "", "Z/Z5[x]")
+testringconvert(j, lring, "((1x^1+-2x^0)/(1x^2+3/8x^1+3/4x^0))y^0", "QQ(x)[y]")
+
+-- testringconvert(k, aring, "", "ZZ")
+-- testringconvert(k, bring, "", "QQ")
+-- testringconvert(k, cring, "", "Z/Z7")
+-- testringconvert(k, dring, "", "Z/Z14")
+testringconvert(k, ering, "1x^3+3x^2+1x^1+0x^0", "ZZ[x]")
+-- testringconvert(k, fring, "", "QQ[x]")
+testringconvert(k, gring, "((1x^3+3x^2+1x^1+0x^0)y^0)z^0", "ZZ[x][y][z]")
+-- testringconvert(k, hring, "", "QQ[z][y][x]")
+testringconvert(k, iring, "(1x^3+3x^2+1x^1+0x^0)/(1x^0)", "ZZ(x)")
+-- testringconvert(k, jring, "", "QQ(x)")
+testringconvert(k, kring, "1x^3+3x^2+1x^1+0x^0", "Z/Z5[x]")
+-- testringconvert(k, lring, "", "QQ(x)[y]")
+
+-- testringconvert(l, aring, "", "ZZ")
+-- testringconvert(l, bring, "", "QQ")
+-- testringconvert(l, cring, "", "Z/Z7")
+-- testringconvert(l, dring, "", "Z/Z14")
+-- testringconvert(l, ering, "", "ZZ[x]")
+-- testringconvert(l, fring, "", "QQ[x]")
+-- testringconvert(l, gring, "", "ZZ[x][y][z]")
+-- testringconvert(l, hring, "", "QQ[z][y][x]")
+-- testringconvert(l, iring, "", "ZZ(x)")
+-- testringconvert(l, jring, "", "QQ(x)")
+-- testringconvert(j, kring, "", "Z/Z5[x]")
+testringconvert(l, lring, "((7/6x^0)/(1x^1+2x^0))y^3+((4x^2+6x^1+6x^0)/(1x^1+3x^0))y^2+((3/2x^0)/(1x^3+8x^2+1/2x^1+6x^0))y^1+((1/4x^1+1x^0)/(1x^1+0x^0))y^0", "QQ(x)[y]")
+
+
+
+
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/rings/modulararithmetic.lua b/macros/luatex/latex/luacas/tex/test/rings/modulararithmetic.lua
new file mode 100644
index 0000000000..1bd315595a
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/rings/modulararithmetic.lua
@@ -0,0 +1,20 @@
+local a = IntegerModN(Integer(5), Integer(3))
+local b = IntegerModN(Integer(1), Integer(3))
+local c = IntegerModN(Integer(-12), Integer(3))
+local f = IntegerModN(Integer(100), Integer(62501))
+local d = IntegerModN(Integer(16), Integer(36))
+local e = IntegerModN(Integer(27), Integer(36))
+
+starttest("modular arithmetic")
+testeq(a, "2")
+testeq(b, "1")
+testeq(c, "0")
+testeq(a + b, "0")
+testeq(a - b, "1")
+testeq(a * b, "2")
+testeq(a:inv(), "2")
+testeq(b:inv(), "1")
+testeq(f:inv(), "61876")
+testeq(d * e, "0")
+testeq(a * d, "2")
+endtest() \ No newline at end of file
diff --git a/macros/luatex/latex/luacas/tex/test/rings/number.lua b/macros/luatex/latex/luacas/tex/test/rings/number.lua
new file mode 100644
index 0000000000..9afea0d364
--- /dev/null
+++ b/macros/luatex/latex/luacas/tex/test/rings/number.lua
@@ -0,0 +1,118 @@
+local a = Integer(5)
+local b = Integer(3)
+local c = Integer(-12)
+local d = Integer("-54321")
+local e = Integer("99989999999999999989999999999999999999999999999999999999989999999999999999999999999998999999999999999999999999989999999998")
+local f = Integer("-1267650600228229401496703205376")
+local g = Integer(16)
+local h = Integer(8)
+local x = Integer(8) / Integer(5)
+local y = Integer(1) / Integer(12)
+local z = Integer(-7) / Integer(10)
+
+
+starttest("integer construction")
+testeq(a, 5)
+testeq(b, 3)
+testeq(c, -12)
+testeq(d, "-54321")
+testeq(e, "99989999999999999989999999999999999999999999999999999999989999999999999999999999999998999999999999999999999999989999999998")
+testeq(f, "-1267650600228229401496703205376")
+endtest()
+
+starttest("integer operations")
+testeq(-c, 12)
+testeq(a + b, 8)
+testeq(b - c, 15)
+testeq(d - d, 0)
+testeq(e + f, "99989999999999999989999999999999999999999999999999999999989999999999999999999999999998999998732349399771770598493296794622")
+testeq(a * c, -60)
+testeq(f * f, "1606938044258990275541962092341162602522202993782792835301376")
+testeq(e * f, "-126752383516820657842978847503263945985032967946239999999987323493997717705985032967944972349399771770598503296781947493995182404784576509143246593589248")
+testeq(a // b, 1)
+testeq(a % b, 2)
+testeq(f // d, "23336289836862896513258283")
+testeq(f % d, "-14533")
+testeq(e // -f, "78878201913048970415230130190415677050906625793723347950240237316957169209243093407705758276")
+testeq(e % -f, "1011644662020502370048160308222")
+testeq(c ^ a, -248832)
+testeq(d ^ a, "-472975648731213834575601")
+testeq(a == b, false)
+testeq(b < a, true)
+testeq(a <= a, true)
+testeq(f < d, true)
+testeq(e <= f, false)
+endtest()
+
+
+
+starttest("integer conversions")
+testeq(a / b, "5/3")
+testeq(g / c, "-4/3")
+testeq(c / b, -4)
+endtest()
+
+
+starttest("rational operations")
+testeq(-x, "-8/5")
+testeq(x + y, "101/60")
+testeq(z - y, "-47/60")
+testeq(x * z, "-28/25")
+testeq(x / y, "96/5")
+testeq(y<x, true)
+testeq(z<z, false)
+testeq(z<=z, true)
+endtest()
+
+starttest("combined integer/rational operations")
+testeq(a + x, "33/5")
+testeq(x + a, "33/5")
+testeq(b - y, "35/12")
+testeq(y - b, "-35/12")
+testeq(c * y, -1)
+testeq(y * c, -1)
+testeq(a / x, "25/8")
+testeq(x / a, "8/25")
+testeq(a/h == x, false)
+testeq(h/a == x, true)
+testeq(y < b , true)
+testeq(b < y, false)
+endtest()
+
+local f = Integer(3)
+local g = Integer(216)
+local h = Integer(945)
+local i = Integer("7766999")
+local j = Integer(4)
+local k = Integer(8)
+local m = Integer(16)
+local n = Integer(100000000003)
+local o = Integer(200250077)
+
+starttest("Miller-Rabin Primes")
+testeq(f:isprime(), true, f)
+testeq(g:isprime(), false, g)
+testeq(h:isprime(), false, h)
+testeq(i:isprime(), false, i)
+testeq(n:isprime(), true, n)
+testeq(o:isprime(), false, o)
+endtest()
+
+
+starttest("Pollard Rho algorithm")
+testeq(f:findafactor(), 3, f)
+testeq(g:findafactor(), 2, g)
+testeq(h:findafactor(), 3, h)
+testeq(i:findafactor(), 41, i)
+testeq(j:findafactor(), 2, j)
+testeq(k:findafactor(), 2, k)
+testeq(m:findafactor(), 2, m)
+endtest()
+
+starttest("prime factorization")
+testeq(f:primefactorization(), "* (3 ^ 1)", f, true)
+testeq(g:primefactorization(), "(2 ^ 3) * (3 ^ 3)", g, true)
+testeq(h:primefactorization(), "(3 ^ 3) * (5 ^ 1) * (7 ^ 1)", h, true)
+testeq(i:primefactorization(), "(41 ^ 1) * (189439 ^ 1)", i, true)
+testeq(o:primefactorization(), "(10007 ^ 1) * (20011 ^ 1)", o, true)
+endtest() \ No newline at end of file