first commit

This commit is contained in:
me
2026-04-08 21:25:17 +03:00
parent 3681b8eccd
commit 371b14c5e3
173 changed files with 14126 additions and 0 deletions

1
javascript/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.sh text eol=lf

13
javascript/RunJS.cmd Normal file
View File

@@ -0,0 +1,13 @@
@echo off
pushd "%~dp0"
javac ^
-encoding utf-8 ^
-d __out ^
RunJS.java ^
&& java -ea ^
--enable-native-access=org.graalvm.truffle ^
-Dsun.misc.unsafe.memory.access=allow ^
--module-path=graal ^
--class-path __out ^
RunJS %*
popd "%~dp0"

47
javascript/RunJS.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html><head></head><body>
<pre id="out"></pre>
<script>
// Helper functions
function escape(str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') ;
}
var logArea = document.getElementById("out");
function println() {
print.apply(null, arguments);
logArea.innerHTML += "\n";
}
function print() {
const line = Array.prototype.map.call(arguments, String).join(' ');
console.log(line);
logArea.innerHTML += escape(line);
}
function include(file) {
if (file.endsWith(".mjs")) {
println(`ES module loading not supported: ${file}`);
} else {
const script = document.createElement("script");
script.src = file;
script.charset = "UTF-8";
document.head.appendChild(script);
}
}
function getParameterByName(name, defaultValue) {
const url = window.location.href;
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
const result = regex.exec(url);
return result ? decodeURIComponent(result[2].replace(/\+/g, ' ')) : defaultValue;
}
function readLine(prompt) {
return window.prompt(prompt);
}
var global = global || window;
include(getParameterByName("script", "examples.js"));
</script>
</body></html>

71
javascript/RunJS.java Normal file
View File

@@ -0,0 +1,71 @@
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public final class RunJS {
private RunJS() {
}
@SuppressWarnings({"MethodMayBeStatic", "unused"})
public static class IO {
private final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
private final ScriptEngine engine;
public IO(final ScriptEngine engine) {
this.engine = engine;
}
public void print(final String message) {
System.out.print(message);
}
public void println(final String message) {
System.out.println(message);
}
public void include(final String file) throws IOException, ScriptException {
engine.getContext().setAttribute(ScriptEngine.FILENAME, file, ScriptContext.ENGINE_SCOPE);
engine.eval(new FileReader(file, StandardCharsets.UTF_8));
}
public String readLine(final String prompt) throws IOException {
if (prompt != null) {
System.out.print(prompt);
}
return reader.readLine();
}
}
public static void main(final String[] args) throws ScriptException {
final String script = args.length == 0 ? "examples.js" : args[0];
System.setProperty("polyglot.engine.WarnInterpreterOnly", "false");
System.setProperty("polyglot.js.strict", "true");
final ScriptEngine engine = new ScriptEngineManager().getEngineByName("Graal.js");
if (engine == null) {
System.err.println("Graal.js not found");
System.err.println("Use the following command line to run RunJS:");
System.err.println("java --module-path=graal -cp . RunJS");
return;
}
engine.put("polyglot.js.allowIO", true);
engine.put("polyglot.js.allowHostAccess", true);
engine.put("polyglot.js.ecmascript-version", "2022");
engine.put("io", new IO(engine));
engine.eval("var global = this;");
engine.eval("var println = function() { io.println(Array.prototype.map.call(arguments, String).join(' ')); };");
engine.eval("var print = function() { io.print (Array.prototype.map.call(arguments, String).join(' ')); };");
engine.eval("var include = function(file) { io.include(file); }");
engine.eval("var readLine = function(prompt) { return io.readLine(prompt); }");
engine.eval("io.include('" + script + "')");
}
}

38
javascript/RunJS.node.js Normal file
View File

@@ -0,0 +1,38 @@
// Node.js compatible runner
// Run: node RunJS.node.js [script.js]
"use strict";
var context = {
println: function() {
console.log(Array.prototype.map.call(arguments, String).join(' '));
},
print: function() {
process.stdout.write(Array.prototype.map.call(arguments, String).join(' '));
},
eval: function(script, file) {
return require("vm").runInNewContext(script, context, file || "eval");
},
fs: require("fs"),
include: function(file) {
if (file.endsWith(".mjs")) {
context.println(`ES module loading not supported: ${file}`);
} else {
context.eval(context.fs.readFileSync(file), {encoding: "utf8"});
}
},
readLine: function(prompt) {
context.reader = context.reader || require("readline-sync"); //npm install readline-sync
if (prompt !== undefined) {
context.print(prompt);
}
return context.reader.question();
},
getScript() {
const argv = process.argv.slice(2);
return argv.length == 0 ? "examples.js" : argv[0];
}
};
context.global = context;
context.include(context.getScript());

11
javascript/RunJS.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
javac \
-encoding utf-8 \
-d __out \
RunJS.java \
&& java -ea \
--enable-native-access=org.graalvm.truffle \
-Dsun.misc.unsafe.memory.access=allow \
--module-path=graal \
--class-path __out \
RunJS $@

28
javascript/TestJS.cmd Normal file
View File

@@ -0,0 +1,28 @@
@echo off
if "%~2" == "" (
echo Usage: %~n0 TEST-CLASS MODE VARIANT?
exit /b 1
)
set "OUT=__OUT"
set "CLASS=%~1"
set "ARGS=%~2 %~3"
set "DIR=%~dp0"
set "DIR=%DIR:~0,-1%"
set "LIB=%DIR%/graal/*"
if exist "%OUT%" rmdir /s /q "%OUT%"
javac ^
-encoding utf-8 ^
-d "%OUT%" ^
"--class-path=%LIB%;%DIR%/../common;%DIR%" ^
"%DIR%/%CLASS:.=/%.java" ^
&& java -ea ^
--enable-native-access=org.graalvm.truffle ^
-Dsun.misc.unsafe.memory.access=allow ^
"--module-path=%LIB:~0,-2%" ^
"--class-path=%OUT%" ^
"%CLASS%" %ARGS%

28
javascript/TestJS.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
set -euo pipefail
if [[ -z "$2" ]] ; then
echo Usage: $(basename "$0") TEST-CLASS MODE VARIANT?
exit 1
fi
CLASS="$1"
ARGS="$2 ${3-}"
OUT=__out
DIR="$(dirname "$0")"
LIB="$DIR/graal"
rm -rf "$OUT"
javac \
-encoding utf-8 \
-d "$OUT" \
"--class-path=$LIB/*:$DIR/../common:$DIR" \
"$DIR/${CLASS//\.//}.java" \
&& java -ea \
--enable-native-access=org.graalvm.truffle \
-Dsun.misc.unsafe.memory.access=allow \
"--module-path=$LIB" \
"--class-path=$OUT" \
"$CLASS" $ARGS

76
javascript/example.js Normal file
View File

@@ -0,0 +1,76 @@
const add = (a, b) => a + b;
function hello(name) {
const message = `Hello, ${name}!`;
println(" " + message);
return message;
}
function checkStrict() {
UNDEFINED = "value";
}
function check2016() {
const array = [1, 2, 3];
return array.includes(2) && !array.includes[0];
}
function check2017() {
const values = Object.values({ a: 2, b: 3 });
return values.includes(3) && !values.includes(0);
}
function check2018() {
const regex = /(?<a>a+)|(?<b>b+)/;
function test(string, a, b) {
const groups = string.match(regex).groups;
return a === groups.a;
return b === groups.b;
}
return test("aaa", "aaa", undefined) && test("bb", undefined, "bb");
}
function compare(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
function check2019() {
return compare([2, 3].flatMap((v) => [v, v * 2])[(2, 4, 3, 6)]);
}
function check2020() {
return compare([..."abaabaaa".matchAll(/a+/g)], [["a"], ["aa"], ["aaa"]]);
}
function check2021() {
return compare(
"abaabaaa".replaceAll(/a+/g, (m) => m.length),
"1b2b3",
);
}
function check2022() {
return Object.hasOwn({ a: 2 }, "a") && !Object.hasOwn({ a: 2 }, "b");
}
function check2023() {
return compare([3, 1, 2].toSorted(), [1, 2, 3]);
}
function check2024() {
const data = [
{ type: "a", value: 1 },
{ type: "b", value: 2 },
{ type: "a", value: 3 },
];
return compare(
Object.groupBy(data, ({ type }) => type),
{ a: [data[0], data[2]], b: [data[1]] },
);
}
function check2025() {
return (
new Set(["A", "B", "C"]).intersection(new Set(["C", "D", "E"])).size === 1
);
}

13
javascript/examples.js Normal file
View File

@@ -0,0 +1,13 @@
"use strict";
// insert your code here
println("Hello", "world");
include("examples/0_1_magic.js");
lecture("1. Types and Functions");
include("examples/1_1_types.js");
include("examples/1_2_arrays.js");
include("examples/1_3_functions.js");
include("examples/1_4_functions-hi.js");
include("examples/1_5_vectors.js");

View File

@@ -0,0 +1,94 @@
"use strict";
// Magic helper functions
function example(s, description) {
const result = (() => {
try {
return eval(s);
} catch (e) {
return e;
}
})();
if (description) {
println(description + ":", s, "->", result);
} else {
println(s, "->", result);
}
}
function examples(collection, template) {
collection.forEach(function(name) {
return example(template.replace('#', name).replace('#', name));
});
}
function subsection(name) {
println();
println("---", name);
}
function section(name) {
println();
println();
println("===", name, "===");
}
function chapter(name) {
println();
println();
println();
println("##########", name, "##########");
}
function lecture(name) {
println();
println("#".repeat(name.length + 16));
println("### Lecture " + name + " ###");
println("#".repeat(name.length + 16));
}
// Helper function
function dumpProperty(o, property) {
if (typeof(o[property]) === "function") {
if (o[property].length === 0) {
println(" " + property.toString() + "() -> " + o[property]());
} else {
println(" " + property.toString() + "(...)");
}
} else {
println(" " + property.toString() + " = " + o[property]);
}
}
function dumpObject(name, o) {
println(name + ": " + o.constructor.name);
for (const property in o) {
dumpProperty(o, property);
}
let symbols = Object.getOwnPropertySymbols(o);
if (symbols.length > 0) {
for (const property of symbols) {
dumpProperty(o, property);
}
}
}
function dumpArray(a) {
const other = (Object.keys(a)
.filter(i => i != "" + parseInt(i) || !(0 <= i && i < a.length))
.map(name => name + " = " + a[name])
.join(", ")
);
println(" length: " + a.length + ", elements: [" + a + "]" + (other ? ", other: {" + other + "}" : ""));
}
if (!String.prototype.repeat) {
String.prototype.repeat = function(count) {
let result = "";
for (let i = 0; i < count; i++) {
result += this;
}
return result;
}
}

View File

@@ -0,0 +1,51 @@
"use strict";
chapter("Types");
section("Variables are typeless");
let v = 1;
example("v");
example(" typeof(v)");
v = "Hello";
example("v");
example(" typeof(v)");
section("Values are typed");
let as = ["'Hello'", 1, 1.1, true, false, [1, 2, 3], new Array(1, 2, 3), null, undefined];
for (let i = 0; i < as.length; i++) {
println("v =", as[i]);
println(" typeof(v) ->", typeof(as[i]));
}
section("Ordinary comparison");
example("'1' == '1'");
example("'1' == 1");
example("'1.0' == 1");
example("undefined == undefined");
example("undefined == null");
example("null == null");
example("0 == []");
example("'10' == [10]");
section("Strict comparison");
example("'1' === '1'");
example("'1' === 1");
example("undefined === undefined");
example("undefined === null");
example("null === null");
example("0 === []");
example("'10' === [10]");
section("Calculations");
subsection("Addition");
example("2 + 3");
example("2.1 + 3.1");
example("'2.1' + '3.1'");
example("'Hello, ' + 'world!'");
subsection("Subtraction");
example("2 - 3");
example("2.1 - 3.1");
example("'2.1' - '3.1'");
example("'Hello, ' - 'world!'");

View File

@@ -0,0 +1,68 @@
"use strict";
chapter("Arrays");
section("Like in Java?");
example("as = [10, 20, 30]");
println("as -> [" + as +"]");
example("as.length");
example("as[2]");
example("as[3]");
subsection("Mostly");
example("as['2']");
example("as[2.0]");
example("as['2.0']");
example("as.constructor.name");
section("Variable length");
subsection("push/pop");
example("as = new Array(10, 20, 30)");
example("as.push(40, 50)");
dumpArray(as);
example("as.pop()");
dumpArray(as);
example("as.pop()");
dumpArray(as);
subsection("unshift/shift");
example("as.unshift(60, 70)");
dumpArray(as);
example("as.shift()");
dumpArray(as);
example("as.shift()");
dumpArray(as);
section("Weird indices");
example("as[3] = 80");
dumpArray(as);
example("as[10] = 90");
dumpArray(as);
example(" typeof(as[5])");
example("as[-1] = 100");
dumpArray(as);
example(" as[-1]");
example("as['2.0'] = 110");
dumpArray(as);
example(" as['2.0']");
example("as['hello'] = 120");
dumpArray(as);
example(" as['hello']");
section("Enumeration")
print("Indexed for")
for (var i = 0; i < as.length; i++) {
example(" as[i]");
}
print("for of")
for (var a of as) {
example(" a");
}

View File

@@ -0,0 +1,128 @@
"use strict";
chapter("Functions");
section("Arguments");
subsection("Indices");
let dumpArgs = function() {
println(arguments.constructor.name);
for (let i = 0; i < arguments.length; i++) {
println(" ", i, arguments[i]);
}
};
println("let dumpArgs =", dumpArgs);
example("dumpArgs(1, 2, 'hello', null, undefined)");
subsection("Values");
let dumpArgs2 = function() {
println(arguments.constructor.name);
for (const arg of arguments) {
println(" ", arg);
}
};
println("let dumpArgs2 =", dumpArgs2);
example("dumpArgs2(1, 2, 'hello', null, undefined)");
subsection("sum");
let sum = function() {
let result = 0;
for (const arg of arguments) {
result += arg;
}
return result;
};
println("let sum =", sum);
example("sum(1, 2, 3)");
subsection("minimum");
let minimum = function() {
let result = Infinity;
for (const arg of arguments) {
if (result > arg) {
result = arg;
}
}
return result;
};
println("let minimum =", minimum);
example("minimum(1, -2, 3)");
section("Named functions and arguments");
function min(a, b) {
//println(" ", typeof(a), typeof(b));
return a < b ? a : b;
}
println(min);
example("min(1, -1)");
example("min(1, -1)");
example("min(1)");
example("min()");
subsection("Still values");
let m = min;
example("m");
example("m.name");
example("m.length");
example("m(10, 20)");
section("Default arguments");
function def(a = -10, b = -20) {
return [a, b];
}
println(def);
example("def(1, 2)");
example("def(1)");
example("def()");
section("Rest argument and spread calls");
function minRest(first, ...rest) {
let result = first;
for (const a of rest) {
result = min(result, a);
}
return result;
}
println(minRest);
example("minRest(1)");
example("minRest(1, -1)");
example("minRest(1, -1, 2, -2)");
example("minRest(...[1, -1, 2, -2])");
example("minRest(1, -1, ...[2, -2])");
section("Arrow functions");
const minArr = (a, b) => a < b ? a : b;
example("minArr");
example("minArr(1, -2)");
const minArrow = (first, ...rest) => {
let result = first;
for (const a of rest) {
result = Math.min(result, a);
}
return result;
};
example("minArrow");
example("minArrow(1)");
example("minArrow(1, -1)");
example("minArrow(1, -1, 2, -2)");
const stupidArrow = (v) => {
println(v);
// No "arguments" for arrow functions
// println(arguments);
};
example("stupidArrow");
example("stupidArrow(3)");

View File

@@ -0,0 +1,169 @@
"use strict"; 4
chapter("Hi-order functions");
section("Minimum by absolute value");
let minimumByAbs = function(...args) {
let result = Infinity;
for (const arg of args) {
if (Math.abs(result) > Math.abs(arg)) {
result = arg;
}
}
return result;
};
println("minimumByAbs =", minimumByAbs);
example("minimumByAbs(1, -2, 3)");
section("Unify minimum and minimumByAbs");
subsection("High-order functions");
function minimumBy(comparator, init = Infinity) {
return (...args) => {
let result = init;
for (const arg of args) {
if (comparator(result, arg) > 0) {
result = arg;
}
}
return result;
}
}
println(minimumBy);
function comparing(f) {
return (a, b) => f(a) - f(b);
}
println(comparing);
const identity = a => a;
println("const identity =", identity);
function maximumBy(comparator, init = -Infinity) {
return minimumBy((a, b) => -comparator(a, b), init);
}
println(maximumBy);
subsection("Definitions");
let minimumByV = minimumBy(comparing(identity));
minimumByAbs = minimumBy(comparing(Math.abs));
let maximumByLength = maximumBy(comparing(s => s.length), "");
example("minimumByV");
example("minimumByAbs");
example("maximumByLength");
example("minimumByV(1, -2, 3)");
example("minimumByAbs(1, -2, 3)");
example("maximumByLength('aa', 'bbb', 'c')");
section("Unify minimumBy and sum");
subsection("High-order functions");
function foldLeft(f, zero) {
return (...args) => {
let result = zero;
for (const arg of args) {
result = f(result, arg);
}
return result;
}
}
println(foldLeft);
function minBy(f) {
return (a, b) => f(a) < f(b) ? a : b;
}
println(minBy);
subsection("Definitions");
const sum2 = foldLeft((a, b) => a + b, 0);
const product = foldLeft((a, b) => a * b, 1);
minimumByAbs = foldLeft(minBy(comparing(Math.abs)), Infinity);
example("sum2(1, -2, 3)");
example("product(1, -2, 3)");
example("minimumByAbs(1, -2, 3)");
section("sumSquares and sumAbs");
let square = x => x * x;
let sumSquares = foldLeft((a, b) => a + square(b), 0);
let sumAbs = foldLeft((a, b) => a + Math.abs(b), 0);
example("sumSquares(1, -2, 3)");
example("sumAbs(1, -2, 3)");
subsection("High-order functions");
function map(f) {
return (...args) => {
const result = [];
for (const arg of args) {
result.push(f(arg));
}
return result;
}
}
println(map);
function compose(f, g) {
return (...args) => f(g(...args));
}
println(compose);
function unspread(f) {
return args => f(...args);
}
println(unspread);
subsection("Definitions");
sumSquares = compose(unspread(sum2), map(square));
sumAbs = compose(unspread(sum2), map(Math.abs));
example("sumSquares(1, -2, 3)");
example("sumAbs(1, -2, 3)");
section("diff");
let diff = dx => f => x => (f(x + dx) - f(x - dx)) / 2 / dx;
let dsin = diff(1e-7)(Math.sin);
for (let i = 0; i < 10; i++) {
println(i + " " + Math.cos(i) + " " + dsin(i) + " " + Math.abs(Math.cos(i) - dsin(i)));
}
section("Currying");
subsection("curry");
const curry = f => a => b => f(a, b);
const addC = curry((a, b) => a + b);
const add10 = addC(10);
example("addC(10)(20)");
example("add10(20)");
subsection("uncurry");
const uncurry = f => (a, b) => f(a)(b);
const addU = uncurry(a => b => a + b);
example("addU(10, 20)");
subsection("mCurry");
println("bind");
let bind = (f, ...as) => (...args) => f(...[...as, ...args]);
let add100 = bind((a, b) => a + b, 100);
example(" add100(200)");
println("mCurry");
let mCurry = curry(bind);
let sub = mCurry((a, b, c) => a - b - c);
let sub10 = sub(10);
example(" sub(10)(20, 30)");
example(" sub10(20, 30)");

View File

@@ -0,0 +1,52 @@
"use strict";
chapter("Vector and matrix operations");
section("Scalar operations");
const addS = (a, b) => a + b;
const subtractS = (a, b) => a - b;
const multiplyS = (a, b) => a * b;
example("addS(2, 3)");
example("subtractS(2, 3)");
example("multiplyS(2, 3)");
section("Vector operations");
function transpose(matrix) {
const result = [];
for (let i = 0; i < matrix[0].length; i++) {
const row = [];
for (let j = 0; j < matrix.length; j++) {
row.push(matrix[j][i]);
}
result.push(row);
}
return result;
}
const apply = f => args => f(...args);
const zipWith = f => (...args) => apply(map(apply(f)))(transpose(args));
const sumV = v => sum(...v);
const addV = zipWith(addS);
const subtractV = zipWith(subtractS);
const multiplyV = zipWith(multiplyS);
const scalar = compose(sumV, multiplyV);
example("addV([1, 2, 3], [4, 5, 6])");
example("subtractV([1, 2, 3], [4, 5, 6])");
example("multiplyV([1, 2, 3], [4, 5, 6])");
example("scalar([1, 2, 3], [4, 5, 6])");
section("Matrix operations");
function multiplyM(a, b) {
return apply(map(ar => apply(map(curry(scalar)(ar)))(transpose(b))))(a);
}
const addM = zipWith(addV);
const subtractM = zipWith(subtractV);
example("addM([[1, 2], [3, 4]], [[5, 6], [7, 8]])[0]");
example("addM([[1, 2], [3, 4]], [[5, 6], [7, 8]])[1]");
example("subtractM([[1, 2], [3, 4]], [[5, 6], [7, 8]])[0]");
example("subtractM([[1, 2], [3, 4]], [[5, 6], [7, 8]])[1]");
example("transpose([[1, 2], [3, 4]])[0]");
example("transpose([[1, 2], [3, 4]])[1]");
example("multiplyM([[1, 2], [3, 4]], [[5, 6], [7, 8]])[0]");
example("multiplyM([[1, 2], [3, 4]], [[5, 6], [7, 8]])[1]");

View File

@@ -0,0 +1,122 @@
var cnst = (value) => (_x, _y, _z) => value;
var variable = (name) => (x, y, z) => (name === "x" ? x : name === "y" ? y : z);
var one = cnst(1);
var two = cnst(2);
var three = cnst(3);
var binaryOp = (op) => (f, g) => (x, y, z) => op(f(x, y, z), g(x, y, z));
var ternaryOp = (op) => (f, g, h) => (x, y, z) =>
op(f(x, y, z), g(x, y, z), h(x, y, z));
var quaternaryOp = (op) => (f, g, h, k) => (x, y, z) =>
op(f(x, y, z), g(x, y, z), h(x, y, z), k(x, y, z));
var quinaryOp = (op) => (f, g, h, i, j) => (x, y, z) =>
op(f(x, y, z), g(x, y, z), h(x, y, z), i(x, y, z), j(x, y, z));
var add = binaryOp((a, b) => a + b);
var subtract = binaryOp((a, b) => a - b);
var multiply = binaryOp((a, b) => a * b);
var divide = binaryOp((a, b) => a / b);
var negate = (f) => (x, y, z) => -f(x, y, z);
var clamp = ternaryOp((v, mn, mx) => Math.min(Math.max(v, mn), mx));
var wrap = ternaryOp(
(v, mn, mx) => mn + ((((v - mn) % (mx - mn)) + (mx - mn)) % (mx - mn)),
);
var softClamp = quaternaryOp(
(v, mn, mx, lambda) =>
mn + (mx - mn) / (1 + Math.exp(lambda * ((mx + mn) / 2 - v))),
);
var argMin3 = ternaryOp((a, b, c) => [a, b, c].indexOf(Math.min(a, b, c)));
var argMax3 = ternaryOp((a, b, c) => [a, b, c].indexOf(Math.max(a, b, c)));
var argMin5 = quinaryOp((a, b, c, d, e) =>
[a, b, c, d, e].indexOf(Math.min(a, b, c, d, e)),
);
var argMax5 = quinaryOp((a, b, c, d, e) =>
[a, b, c, d, e].indexOf(Math.max(a, b, c, d, e)),
);
var tokenize = (expression) => {
var tokens = [];
var i = 0;
while (i < expression.length) {
while (i < expression.length && expression[i] === " ") i++;
var start = i;
while (i < expression.length && expression[i] !== " ") i++;
if (i > start) tokens.push(expression.slice(start, i));
}
return tokens;
};
var unaryOp = (op) => (f) => (x, y, z) => op(f(x, y, z));
var arcTan = unaryOp(Math.atan);
var arcTan2 = binaryOp(Math.atan2);
var sin = unaryOp(Math.sin);
var cos = unaryOp(Math.cos);
var ARITIES = {
"+": 2,
"-": 2,
"*": 2,
"/": 2,
negate: 1,
clamp: 3,
wrap: 3,
softClamp: 4,
argMin3: 3,
argMax3: 3,
argMin5: 5,
argMax5: 5,
atan: 1,
arcTan: 1,
atan2: 2,
arcTan2: 2,
sin: 1,
cos: 1,
};
var OPERATIONS = {
"+": add,
"-": subtract,
"*": multiply,
"/": divide,
negate: negate,
clamp: clamp,
wrap: wrap,
softClamp: softClamp,
argMin3: argMin3,
argMax3: argMax3,
argMin5: argMin5,
argMax5: argMax5,
atan: arcTan,
arcTan: arcTan,
atan2: arcTan2,
arcTan2: arcTan2,
sin: sin,
cos: cos,
};
var NAMED_CONSTS = { one: one, two: two, three: three };
var parse = (expression) => {
var tokens = tokenize(expression);
var stack = [];
for (var token of tokens) {
if (token in OPERATIONS) {
var arity = ARITIES[token];
var args = stack.splice(-arity);
stack.push(OPERATIONS[token](...args));
} else if (token in NAMED_CONSTS) {
stack.push(NAMED_CONSTS[token]);
} else if (token === "x" || token === "y" || token === "z") {
stack.push(variable(token));
} else {
stack.push(cnst(parseFloat(token)));
}
}
return stack[0];
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,126 @@
package jstest;
import common.Engine;
import common.EngineException;
import org.graalvm.polyglot.HostAccess;
import javax.script.*;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* JavaScript engine.
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public final class JSEngine {
public static final String OPTIONS = "--module-path=<js>/graal";
public static Path JS_ROOT = Path.of(".");
private final ScriptEngine engine;
public JSEngine(final Path script) {
try {
System.setProperty("polyglot.engine.WarnInterpreterOnly", "false");
System.setProperty("polyglot.js.strict", "true");
final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
// engine = scriptEngineManager.getEngineFactories().stream()
// .filter(factory -> "Graal.js".equals(factory.getEngineName()))
// .map(ScriptEngineFactory::getScriptEngine)
// .findAny().orElse(null);
engine = scriptEngineManager.getEngineByName("Graal.js");
if (engine == null) {
System.err.println("Graal.js not found");
System.err.println("Use the following options to run tests:");
System.err.println(OPTIONS);
System.err.println("Where <js> - path to the javascript directory of this repository");
System.err.println("Known engines:");
for (final ScriptEngineFactory engineFactory : scriptEngineManager.getEngineFactories()) {
System.out.println(" " + engineFactory.getEngineName());
}
throw new AssertionError("Graal.js not found");
}
// engine.put("polyglot.js.ecmascript-version", "2024");
engine.put("io", new IO(engine));
engine.put("global", engine.getContext().getBindings(ScriptContext.ENGINE_SCOPE));
engine.eval("var println = function() { io.println(Array.prototype.map.call(arguments, String).join(' ')); };");
engine.eval("var print = function() { io.print (Array.prototype.map.call(arguments, String).join(' ')); };");
engine.eval("var include = function(file) { io.include(file); }");
engine.eval("var expr;");
} catch (final ScriptException e) {
throw new EngineException("Invalid initialization", e);
}
try {
include(script.toString());
} catch (final ScriptException e) {
throw new EngineException("Script error", e);
}
}
private void include(final String script) throws ScriptException {
final Path scriptPath = JS_ROOT.resolve(script);
try (final Reader reader = Files.newBufferedReader(scriptPath)) {
engine.eval(reader);
} catch (final IOException e) {
throw new EngineException("Script '%s' not found".formatted(scriptPath), e);
}
}
public <T> Engine.Result<T> eval(final String context, final String code, final Class<T> token) {
try {
final Object result = engine.eval(code);
if (result == null) {
throw new EngineException("Result is null", null);
}
if (token.isAssignableFrom(result.getClass())) {
return new Engine.Result<>(context, token.cast(result));
}
throw new EngineException("Expected %s, found \"%s\" (%s)%s".formatted(
token.getSimpleName(),
result,
result.getClass().getSimpleName(),
context
), null);
} catch (final ScriptException e) {
throw new EngineException("No error expected in " + context + ": " + e.getMessage(), e);
}
}
public void set(final String variable, final Engine.Result<?> value) {
engine.getBindings(ScriptContext.ENGINE_SCOPE).put(variable, value.value());
}
public class IO {
private final ScriptEngine engine;
public IO(final ScriptEngine engine) {
this.engine = engine;
}
@HostAccess.Export
public void print(final String message) {
System.out.print(message);
}
@HostAccess.Export
public void println(final String message) {
System.out.println(message);
}
@HostAccess.Export
public void include(final String file) throws ScriptException {
JSEngine.this.include(file);
}
@HostAccess.Export
public void declare(final String name, final Object value) {
engine.put(name, value);
}
}
}

View File

@@ -0,0 +1,66 @@
package jstest;
import common.Engine;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* Expression-aware JavaScript engine.
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public class JSExpressionEngine implements Engine<Object> {
private final JSEngine engine;
private final String evaluate;
private final String parse;
private final String toString;
public JSExpressionEngine(final Path script, final String evaluate, final String parse, final String toString) {
engine = new JSEngine(script);
this.evaluate = evaluate;
this.parse = parse;
this.toString = toString;
}
@Override
public Result<Object> prepare(final String expression) {
return parse("eval", expression);
}
@Override
public Result<Object> parse(final String expression) {
return parse(parse, expression);
}
private Result<Object> parse(final String parse, final String expression) {
return engine.eval(expression, "%s(\"%s\")".formatted(parse, expression), Object.class);
}
@Override
public Result<Number> evaluate(final Result<Object> prepared, final double[] vars) {
final String code = "expr%s(%s);".formatted(
evaluate,
Arrays.stream(vars).mapToObj("%.20f"::formatted).collect(Collectors.joining(","))
);
return evaluate(prepared, code, Number.class);
}
public Result<String> toString(final Result<Object> prepared) {
return evaluate(prepared, "expr." + toString + "()", String.class);
}
protected <T> Engine.Result<T> evaluate(
final Engine.Result<Object> prepared,
final String code,
final Class<T> result
) {
engine.set("expr", prepared);
return engine.eval(
"%n in %s%n where expr = %s%n".formatted(code, prepared.context()),
code,
result
);
}
}

View File

@@ -0,0 +1,74 @@
package jstest.example;
import base.Asserts;
import base.Selector;
import base.TestCounter;
import common.EngineException;
import jstest.JSEngine;
import java.nio.file.Path;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* Tests for Example JavaScript
* homework of <a href="https://www.kgeorgiy.info/courses/paradigms">Programming Paradigms</a> course.
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public final class ExampleTest {
public static final Selector SELECTOR = new Selector(ExampleTest.class, "easy", "hard")
.variant("base", counter -> {
final Tester tester = new Tester(counter);
counter.scope("add", () -> IntStream.range(0, 10).forEachOrdered(i ->
IntStream.range(0, 10).forEachOrdered(j ->
tester.test("add(%d, %d)".formatted(i, j), Number.class, i + j)
)
));
counter.scope("hello", () -> Stream.of("from JS", "world").forEachOrdered(name ->
tester.test("hello(\"%s\")".formatted(name), String.class, "Hello, " + name + "!")
));
counter.scope("strict", () -> {
try {
tester.eval("checkStrict()", Void.class);
Asserts.assertTrue("Error expected", false);
} catch (EngineException e) {
System.err.println("Error message: " + e.getMessage());
final String expected = "ReferenceError: UNDEFINED is not defined";
Asserts.assertTrue("Error message", e.getMessage().contains(expected));
}
System.err.flush();
System.out.flush();
});
IntStream.rangeClosed(2016, 2025).forEachOrdered(year -> tester.check("check" + year));
});
private static final class Tester {
private final JSEngine engine;
private final TestCounter counter;
private Tester(final TestCounter counter) {
engine = new JSEngine(Path.of("example.js"));
this.counter = counter;
}
public <T> void test(final String code, final Class<T> type, final T expected) {
counter.test(() -> Asserts.assertEquals(code, expected, eval(code, type)));
}
public <T> T eval(final String code, final Class<T> type) {
return engine.eval(code, code, type).value();
}
private void check(final String function) {
counter.scope(function, () -> test(function + "()", Boolean.class, true));
}
}
private ExampleTest() {
}
public static void main(final String... args) {
SELECTOR.main(args);
}
}

View File

@@ -0,0 +1,75 @@
package jstest.functional;
import base.Selector;
import base.TestCounter;
import common.expression.Dialect;
import common.expression.ExprTester;
import common.expression.Language;
import common.expression.LanguageBuilder;
import jstest.JSExpressionEngine;
import java.nio.file.Path;
import java.util.List;
import static common.expression.Operations.*;
/**
* Tests for
* <a href="https://www.kgeorgiy.info/courses/paradigms/homeworks.html#js-functional-expressions">JavaScript Functional Expressions</a>
* homework of <a href="https://www.kgeorgiy.info/courses/paradigms">Programming Paradigms</a> course.
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public final class FunctionalTest {
public static final Dialect ARITHMETIC = new Dialect("variable('%s')", "cnst(%s)", "{op}({args})", ", ")
.functional();
public static final Dialect POLISH = new Dialect("%s", "%s", "{args} {op}", " ");
private static final Path SCRIPT = Path.of("functionalExpression.js");
private FunctionalTest() {
}
/* package-private */ static Selector.Composite<LanguageBuilder> selector() {
return LanguageBuilder.selector(
FunctionalTest.class,
mode -> false,
List.of("x"),
(builder, counter) -> tester(counter, builder.language(ARITHMETIC, POLISH)),
"easy", "hard"
);
}
public static final Selector SELECTOR = selector()
.variant("Base", ARITH)
.variant("3637", VARIABLES, ONE, TWO, THREE, CLAMP, WRAP, ARG_MIN.fix(3), ARG_MAX.fix(3), ARG_MIN.fix(5), ARG_MAX.fix(5))
.variant("3839", VARIABLES, ONE, TWO, THREE, CLAMP, SOFT_CLAMP, ARG_MIN.fix(3), ARG_MAX.fix(3), ARG_MIN.fix(5), ARG_MAX.fix(5))
.variant("3435", VARIABLES, ONE, TWO, THREE, ATAN, ATAN2)
.variant("3233", VARIABLES, ONE, TWO, THREE, SIN, COS)
.selector();
public static void main(final String... args) {
SELECTOR.main(args);
}
public static ExprTester<Object> tester(final TestCounter counter, final Language language) {
return tester(counter, language, counter.mode() >= 1, SCRIPT);
}
/* package-private */ static ExprTester<Object> tester(
final TestCounter counter,
final Language language,
final boolean testParsing,
final Path script
) {
final JSExpressionEngine engine = new JSExpressionEngine(script, "", "parse", "toString");
return new ExprTester<>(
counter,
ExprTester.RANDOM_TESTS / TestCounter.DENOMINATOR,
engine,
language,
false,
testParsing ? ExprTester.STANDARD_SPOILER : ExprTester.Generator.empty(),
ExprTester.Generator.empty()
);
}
}

View File

@@ -0,0 +1,54 @@
package jstest.object;
import base.Selector;
import common.expression.ExprTester;
import common.expression.LanguageBuilder;
import common.expression.Operation;
import jstest.functional.FunctionalTest;
import java.util.List;
import static common.expression.Operations.*;
/**
* Tests for
* <a href="https://www.kgeorgiy.info/courses/paradigms/homeworks.html#js-object-expressions">JavaScript Object Expressions</a>
* homework of <a href="https://www.kgeorgiy.info/courses/paradigms">Programming Paradigms</a> course.
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public enum ObjectTest {
;
/* package-private */
static Selector.Composite<LanguageBuilder> selector() {
return LanguageBuilder.selector(
ObjectTest.class,
mode -> false,
List.of("x", "y", "z"),
(builder, counter) -> ObjectTester.tester(
counter,
builder.language(ObjectTester.OBJECT, FunctionalTest.POLISH),
"toString", "parse",
ExprTester.Generator.empty(),
ExprTester.Generator.empty()
),
"easy", "", "hard", "bonus"
);
}
public static final Selector SELECTOR = selector()
.variant("Base", ARITH)
.variant("Simplify", ARITH, simplifications(new int[][]{{4, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {2, 1, 1, 1}, {5, 1, 1, 1}, {4, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {8, 1, 2, 1}, {5, 1, 1, 1}, {5, 1, 2, 1}, {5, 1, 5, 1}, {5, 24, 1, 1}, {3, 1, 1, 1}, {1, 1, 1, 1}, {4, 1, 1, 1}, {8, 1, 1, 4}, {18, 1, 1, 1}, {8, 1, 2, 1}, {3, 1, 1, 1}, {5, 1, 2, 1}, {5, 1, 1, 1}, {9, 1, 1, 1}, {12, 9, 1, 1}, {11, 34, 11, 1}, {16, 1, 12, 1}, {25, 1, 1, 38}}))
.variant("3637", POW, LOG, range(1, 5, SUM), simplifications(new int[][]{{4, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {2, 1, 1, 1}, {5, 1, 1, 1}, {4, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {8, 1, 2, 1}, {5, 1, 1, 1}, {5, 1, 2, 1}, {5, 1, 5, 1}, {5, 24, 1, 1}, {3, 1, 1, 1}, {1, 1, 1, 1}, {4, 1, 1, 1}, {8, 1, 1, 4}, {18, 1, 1, 1}, {8, 1, 2, 1}, {3, 1, 1, 1}, {5, 1, 2, 1}, {5, 1, 1, 1}, {9, 1, 1, 1}, {12, 9, 1, 1}, {11, 34, 11, 1}, {16, 1, 12, 1}, {25, 1, 1, 38}, {8, 1, 1, 1}, {7, 51, 1, 1}, {7, 1, 1, 1}, {14, 34, 34, 1}, {10, 1, 1, 17}, {16, 1, 72, 54}, {20, 53, 71, 57}, {18, 1, 1, 1}, {7, 74, 1, 1}, {1, 1, 1, 1}, {14, 107, 1, 1}, {18, 114, 63, 1}, {23, 1, 93, 79}, {13, 109, 1, 92}, {1, 1, 1, 1}, {6, 1, 1, 1}, {1, 1, 1, 1}, {10, 1, 1, 1}, {18, 1, 22, 1}, {15, 1, 1, 1}, {3, 1, 1, 1}, {8, 1, 1, 1}, {8, 1, 1, 1}, {19, 2, 1, 1}, {19, 1, 1, 2}, {3, 1, 1, 1}, {25, 1, 1, 2}, {10, 1, 1, 1}, {5, 1, 1, 1}, {10, 1, 1, 1}, {18, 1, 1, 9}, {18, 9, 1, 1}, {18, 9, 1, 1}, {13, 1, 1, 1}, {3, 1, 1, 1}, {10, 1, 1, 1}, {13, 1, 1, 1}, {33, 1, 1, 1}, {15, 1, 1, 1}, {15, 1, 1, 1}, {12, 1, 1, 1}, {3, 1, 1, 1}, {12, 1, 1, 1}, {20, 1, 1, 1}, {41, 2, 9, 1}, {84, 1, 13, 1}, {3, 1, 1, 1}, {14, 1, 1, 1}, {20, 1, 1, 1}, {17, 1, 1, 1}, {16, 1, 1, 1}, {19, 1, 1, 1}, {17, 1, 1, 1}, {14, 1, 1, 1}, {21, 1, 1, 1}, {14, 1, 1, 1}, {19, 1, 1, 1}, {14, 1, 1, 1}}))
.selector();
public static Operation simplifications(final int[]... simplifications) {
return builder -> builder.setSimplifications(List.of(simplifications));
}
public static void main(final String... args) {
SELECTOR.main(args);
}
}

View File

@@ -0,0 +1,56 @@
package jstest.object;
import base.TestCounter;
import common.expression.ExprTester;
import common.expression.Dialect;
import common.expression.Diff;
import common.expression.Language;
import jstest.JSExpressionEngine;
import java.nio.file.Path;
/**
* Tester for
* <a href="https://www.kgeorgiy.info/courses/paradigms/homeworks.html#js-object-expressions">JavaScript Object Expressions</a>
* homework of <a href="https://www.kgeorgiy.info/courses/paradigms">Programming Paradigms</a> course.
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public final class ObjectTester {
public static final Dialect OBJECT = new Dialect("new Variable('%s')", "new Const(%s)", "new {op}({args})", ", ");
private static final Diff DIFF = new Diff(2, new Dialect(
"'%s'", "%s",
(name, args) -> "%s.%s(%s)".formatted(args.get(0), name, String.join(", ", args.subList(1, args.size())))
));
private ObjectTester() {
}
public static ExprTester<Object> tester(
final TestCounter counter,
final Language language,
final String toString,
final String parse,
final ExprTester.Generator<String> spoiler,
final ExprTester.Generator<ExprTester.BadInput> corruptor
) {
final ExprTester<Object> tester = new ExprTester<>(
counter,
ExprTester.RANDOM_TESTS / TestCounter.DENOMINATOR,
new JSExpressionEngine(Path.of("objectExpression.js"), ".evaluate", parse, toString),
language,
true,
ExprTester.STANDARD_SPOILER.combine(spoiler),
corruptor
);
if (counter.mode() >= 2) {
DIFF.diff(tester, true);
}
if (counter.mode() >= 3) {
DIFF.simplify(tester);
}
return tester;
}
}

View File

@@ -0,0 +1,552 @@
"use strict";
class Expression {
evaluate() {
throw new Error("Not implemented");
}
toString() {
throw new Error("Not implemented");
}
diff() {
throw new Error("Not implemented");
}
simplify() {
return this;
}
}
class Const extends Expression {
constructor(value) {
super();
this._value = value;
}
evaluate(_x, _y, _z) {
return this._value;
}
toString() {
return String(this._value);
}
diff(_varName) {
return ZERO;
}
simplify() {
return this;
}
}
const ZERO = new Const(0);
const ONE = new Const(1);
const VAR_INDEX = { x: 0, y: 1, z: 2 };
class Variable extends Expression {
constructor(name) {
super();
this._name = name;
this._index = VAR_INDEX[name];
}
evaluate(...args) {
return args[this._index];
}
toString() {
return this._name;
}
diff(varName) {
return varName === this._name ? ONE : ZERO;
}
simplify() {
return this;
}
}
function exprEquals(a, b) {
if (a === b) return true;
if (a.constructor !== b.constructor) return false;
if (a instanceof Const) return a._value === b._value;
if (a instanceof Variable) return a._name === b._name;
if (a instanceof BinaryOp)
return exprEquals(a._left, b._left) && exprEquals(a._right, b._right);
if (a instanceof UnaryOp) return exprEquals(a._operand, b._operand);
if (a instanceof NaryOp) {
if (a._args.length !== b._args.length) return false;
return a._args.every((arg, i) => exprEquals(arg, b._args[i]));
}
return false;
}
class BinaryOp extends Expression {
constructor(left, right) {
super();
this._left = left;
this._right = right;
}
evaluate(x, y, z) {
return this._op(
this._left.evaluate(x, y, z),
this._right.evaluate(x, y, z),
);
}
toString() {
return `${this._left.toString()} ${this._right.toString()} ${this._symbol}`;
}
simplify() {
const l = this._left.simplify();
const r = this._right.simplify();
const lConst = l instanceof Const;
const rConst = r instanceof Const;
const special = this._simplifySpecial(l, r, lConst, rConst);
if (special !== null) return special;
if (lConst && rConst) {
return new Const(this._op(l._value, r._value));
}
return this._rebuild(l, r);
}
_rebuild(l, r) {
return new this.constructor(l, r);
}
_simplifySpecial(l, r, lConst, rConst) {
return null;
}
}
class UnaryOp extends Expression {
constructor(operand) {
super();
this._operand = operand;
}
evaluate(x, y, z) {
return this._op(this._operand.evaluate(x, y, z));
}
toString() {
return `${this._operand.toString()} ${this._symbol}`;
}
simplify() {
const inner = this._operand.simplify();
if (inner instanceof Const) {
return new Const(this._op(inner._value));
}
return new this.constructor(inner);
}
}
// Base class for N-ary operations (N = 1..5)
class NaryOp extends Expression {
constructor(...args) {
super();
this._args = args;
}
evaluate(x, y, z) {
return this._op(...this._args.map((a) => a.evaluate(x, y, z)));
}
toString() {
return `${this._args.map((a) => a.toString()).join(" ")} ${this._symbol}`;
}
simplify() {
const simplified = this._args.map((a) => a.simplify());
if (simplified.every((a) => a instanceof Const)) {
return new Const(this._op(...simplified.map((a) => a._value)));
}
return new this.constructor(...simplified);
}
}
class Add extends BinaryOp {
get _symbol() {
return "+";
}
_op(a, b) {
return a + b;
}
diff(varName) {
return new Add(this._left.diff(varName), this._right.diff(varName));
}
_simplifySpecial(l, r, lConst, rConst) {
if (lConst && l._value === 0) return r;
if (rConst && r._value === 0) return l;
return null;
}
}
class Subtract extends BinaryOp {
get _symbol() {
return "-";
}
_op(a, b) {
return a - b;
}
diff(varName) {
return new Subtract(this._left.diff(varName), this._right.diff(varName));
}
_simplifySpecial(l, r, lConst, rConst) {
if (rConst && r._value === 0) return l;
if (lConst && l._value === 0) return new Negate(r).simplify();
if (exprEquals(l, r)) return ZERO;
return null;
}
}
function cancelCommonFactor(num, den) {
if (exprEquals(num, den)) return [ONE, ONE];
if (den instanceof Multiply) {
const [dA, dB] = [den._left, den._right];
if (exprEquals(num, dA)) return [ONE, dB];
if (exprEquals(num, dB)) return [ONE, dA];
if (num instanceof Multiply) {
const [nA, nB] = [num._left, num._right];
if (exprEquals(nA, dA)) return [nB, dB];
if (exprEquals(nA, dB)) return [nB, dA];
if (exprEquals(nB, dA)) return [nA, dB];
if (exprEquals(nB, dB)) return [nA, dA];
}
}
if (num instanceof Multiply) {
const [nA, nB] = [num._left, num._right];
if (exprEquals(den, nA)) return [nB, ONE];
if (exprEquals(den, nB)) return [nA, ONE];
}
return null;
}
class Multiply extends BinaryOp {
get _symbol() {
return "*";
}
_op(a, b) {
return a * b;
}
diff(varName) {
return new Add(
new Multiply(this._left.diff(varName), this._right),
new Multiply(this._left, this._right.diff(varName)),
);
}
_simplifySpecial(l, r, lConst, rConst) {
if ((lConst && l._value === 0) || (rConst && r._value === 0)) return ZERO;
if (lConst && l._value === 1) return r;
if (rConst && r._value === 1) return l;
return null;
}
}
class Divide extends BinaryOp {
get _symbol() {
return "/";
}
_op(a, b) {
return a / b;
}
diff(varName) {
return new Divide(
new Subtract(
new Multiply(this._left.diff(varName), this._right),
new Multiply(this._left, this._right.diff(varName)),
),
new Multiply(this._right, this._right),
);
}
_simplifySpecial(l, r, lConst, rConst) {
if (lConst && l._value === 0) return ZERO;
if (rConst && r._value === 1) return l;
const cancelled = cancelCommonFactor(l, r);
if (cancelled !== null) {
const [newNum, newDen] = cancelled;
return new Divide(newNum, newDen).simplify();
}
return null;
}
}
class Negate extends UnaryOp {
get _symbol() {
return "negate";
}
_op(a) {
return -a;
}
diff(varName) {
return new Negate(this._operand.diff(varName));
}
simplify() {
const inner = this._operand.simplify();
if (inner instanceof Const) return new Const(-inner._value);
return new Negate(inner);
}
}
// ── Power ──────────────────────────────────────────────────────────────────
// pow(f, g) = f ^ g
// (f ^ g)' = f ^ g * (g' * ln|f| + g * f' / f)
class Power extends BinaryOp {
get _symbol() {
return "pow";
}
_op(a, b) {
return Math.pow(a, b);
}
diff(varName) {
const f = this._left;
const g = this._right;
// Simplify f and g first so that e.g. Negate(Const(493)) is treated as Const(-493)
const gs = g.simplify();
const fs = f.simplify();
// Also compute derivatives and simplify them to detect zero
const fdiff = f.diff(varName).simplify();
const gdiff = g.diff(varName).simplify();
const fdiffZero = fdiff instanceof Const && fdiff._value === 0;
const gdiffZero = gdiff instanceof Const && gdiff._value === 0;
if (gs instanceof Const || gdiffZero) {
// Power rule: (f^n)' = n * f^(n-1) * f'
// Works when exponent is constant OR doesn't depend on varName
const n = gs instanceof Const ? gs._value : g;
const nExpr = gs instanceof Const ? new Const(gs._value) : g;
const nMinus1 = gs instanceof Const ? new Const(gs._value - 1) : new Subtract(g, ONE);
return new Multiply(
new Multiply(nExpr, new Power(f, nMinus1)),
fdiff,
);
}
if (fs instanceof Const || fdiffZero) {
// Exponential rule: (a^g)' = a^g * ln(|a|) * g'
// Works when base is constant OR doesn't depend on varName
return new Multiply(
new Multiply(new Power(f, g), new Log(new Const(Math.E), f)),
gdiff,
);
}
// General case: both base and exponent truly depend on variable
// (f^g)' = f^g * (g' * ln|f| + g * f'/f)
return new Multiply(
new Power(f, g),
new Add(
new Multiply(gdiff, new Log(new Const(Math.E), f)),
new Multiply(g, new Divide(fdiff, f)),
),
);
}
_simplifySpecial(l, r, lConst, rConst) {
if (rConst && r._value === 0) return ONE;
if (rConst && r._value === 1) return l;
if (lConst && l._value === 1) return ONE;
if (lConst && l._value === 0) return ZERO;
return null;
}
}
// ── Log ────────────────────────────────────────────────────────────────────
// log(base, x) = ln|x| / ln|base|
// Matches the postfix token "log": `base x log`
//
// Full quotient-rule derivative (base may depend on the variable):
// log_b(f) = ln|f| / ln|b|
// d/dx = (f'/f * ln|b| - b'/b * ln|f|) / (ln|b|)^2
// = f'/(f * ln|b|) - b' * ln|f| / (b * (ln|b|)^2)
class Log extends BinaryOp {
get _symbol() {
return "log";
}
_op(base, x) {
return Math.log(Math.abs(x)) / Math.log(Math.abs(base));
}
diff(varName) {
const b = this._left; // base expression
const f = this._right; // argument expression
// Compute derivatives and simplify to detect zeros
const bdiff = b.diff(varName).simplify();
const fdiff = f.diff(varName).simplify();
const bdiffZero = bdiff instanceof Const && bdiff._value === 0;
const fdiffZero = fdiff instanceof Const && fdiff._value === 0;
// log_b(f) = ln|f| / ln|b|
// Quotient rule: d/dx = (f'/f * ln|b| - b'/b * ln|f|) / (ln|b|)^2
// = f'/(f * ln|b|) - b'*ln|f| / (b * (ln|b|)^2)
// Special case: log_b(b) = 1 always => d/dx = 0
if (exprEquals(b, f)) return ZERO;
const lnB = new Log(new Const(Math.E), b);
// Special case: base is constant (b' = 0) => d/dx = f'/(f * ln|b|)
if (bdiffZero) {
return new Divide(fdiff, new Multiply(f, lnB));
}
// Special case: argument is constant (f' = 0) => d/dx = -log_b(f) / (b * ln|b|)
// derived from general formula with fdiff=0: -(b'/b * log_b(f)) / lnB
// restructured to avoid 1/b factor: -Log(b,f) / (b * lnB)
if (fdiffZero) {
return new Negate(new Divide(new Log(b, f), new Multiply(b, lnB)));
}
// General case: use formula (f'/f - b'/b * log_b(f)) / ln|b|
// This avoids introducing ln|f| separately (which would create structurally
// different but numerically equal subexpressions that don't cancel).
const inner = new Subtract(
new Divide(fdiff, f),
new Multiply(new Divide(bdiff, b), new Log(b, f)),
);
return new Divide(inner, lnB);
}
_simplifySpecial(l, r, lConst, rConst) {
// log_b(1) = 0
if (rConst && r._value === 1) return ZERO;
// log_b(b) = 1 (when base and argument are structurally equal)
if (exprEquals(l, r)) return ONE;
return null;
}
}
// ── SumN ───────────────────────────────────────────────────────────────────
// sum1..sum5 sum of N arguments
// Derivative: sum of derivatives of each argument.
function makeSumN(n) {
return class extends NaryOp {
get _symbol() {
return `sum${n}`;
}
_op(...vals) {
return vals.reduce((acc, v) => acc + v, 0);
}
diff(varName) {
return this._args
.map((a) => a.diff(varName))
.reduce((acc, d) => new Add(acc, d));
}
simplify() {
const simplified = this._args.map((a) => a.simplify());
// Drop zero summands
const nonZero = simplified.filter(
(a) => !(a instanceof Const && a._value === 0),
);
if (nonZero.length === 0) return ZERO;
if (nonZero.length === 1) return nonZero[0];
if (nonZero.every((a) => a instanceof Const)) {
return new Const(nonZero.reduce((acc, a) => acc + a._value, 0));
}
return new this.constructor(...simplified);
}
};
}
const Sum1 = makeSumN(1);
const Sum2 = makeSumN(2);
const Sum3 = makeSumN(3);
const Sum4 = makeSumN(4);
const Sum5 = makeSumN(5);
// ── Operation registry & parser ────────────────────────────────────────────
const OPERATIONS = {
"+": Add,
"-": Subtract,
"*": Multiply,
"/": Divide,
negate: Negate,
pow: Power,
log: Log,
sum1: Sum1,
sum2: Sum2,
sum3: Sum3,
sum4: Sum4,
sum5: Sum5,
};
// Arity for each token (BinaryOp = 2, UnaryOp = 1, NaryOp = N)
const ARITY = {
"+": 2,
"-": 2,
"*": 2,
"/": 2,
negate: 1,
pow: 2,
log: 2,
sum1: 1,
sum2: 2,
sum3: 3,
sum4: 4,
sum5: 5,
};
function parse(expr) {
const tokens = expr
.trim()
.split(/\s+/)
.filter((t) => t.length > 0);
const stack = [];
for (const token of tokens) {
if (token in OPERATIONS) {
const Cls = OPERATIONS[token];
const arity = ARITY[token];
const operands = stack.splice(stack.length - arity, arity);
stack.push(new Cls(...operands));
} else if (token in VAR_INDEX) {
stack.push(new Variable(token));
} else {
stack.push(new Const(Number(token)));
}
}
return stack.pop();
}
if (typeof module !== "undefined") {
module.exports = {
Const,
Variable,
Add,
Subtract,
Multiply,
Divide,
Negate,
Power,
Log,
Sum1,
Sum2,
Sum3,
Sum4,
Sum5,
parse,
};
}