diff --git a/artifacts/queue/ArrayQueueTest.jar b/artifacts/queue/ArrayQueueTest.jar new file mode 100644 index 0000000..fa5a90c Binary files /dev/null and b/artifacts/queue/ArrayQueueTest.jar differ diff --git a/artifacts/queue/QueueTest.jar b/artifacts/queue/QueueTest.jar new file mode 100644 index 0000000..aca8315 Binary files /dev/null and b/artifacts/queue/QueueTest.jar differ diff --git a/artifacts/search/BinarySearchTest.jar b/artifacts/search/BinarySearchTest.jar new file mode 100644 index 0000000..cac98cb Binary files /dev/null and b/artifacts/search/BinarySearchTest.jar differ diff --git a/common/base/Asserts.java b/common/base/Asserts.java new file mode 100644 index 0000000..df43a1e --- /dev/null +++ b/common/base/Asserts.java @@ -0,0 +1,92 @@ +package base; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("StaticMethodOnlyUsedInOneClass") +public final class Asserts { + static { + Locale.setDefault(Locale.US); + } + + private Asserts() { + } + + public static void assertEquals(final String message, final Object expected, final Object actual) { + final String reason = String.format("%s:%n expected `%s`,%n actual `%s`", + message, toString(expected), toString(actual)); + assertTrue(reason, Objects.deepEquals(expected, actual)); + } + + public static String toString(final Object value) { + if (value != null && value.getClass().isArray()) { + final String result = Arrays.deepToString(new Object[]{value}); + return result.substring(1, result.length() - 1); + } else { + return Objects.toString(value); + } + } + + public static void assertEquals(final String message, final List expected, final List actual) { + for (int i = 0; i < Math.min(expected.size(), actual.size()); i++) { + assertEquals(message + ":" + (i + 1), expected.get(i), actual.get(i)); + } + assertEquals(message + ": Number of items", expected.size(), actual.size()); + } + + public static void assertTrue(final String message, final boolean value) { + if (!value) { + throw error("%s", message); + } + } + + public static void assertEquals(final String message, final double expected, final double actual, final double precision) { + assertTrue( + String.format("%s: Expected %.12f, found %.12f", message, expected, actual), + isEqual(expected, actual, precision) + ); + } + + private static boolean is(final double expected, final double actual, final double precision) { + return Math.abs(expected - actual) < precision; + } + + public static boolean isEqual(final double expected, final double actual, final double precision) { + final double error = Math.abs(actual - expected); + return error <= precision + || error <= precision * Math.abs(expected) + || !Double.isFinite(expected) + || Math.abs(expected) > 1e100 + || Math.abs(expected) < precision && !Double.isFinite(actual) + || is(Math.PI, expected, precision) && is(0, actual, precision) + || is(0, expected, precision) && is(Math.PI, actual, precision) + || is(-Math.PI, expected, precision) && is(Math.PI, actual, precision) + || is(Math.PI, expected, precision) && is(-Math.PI, actual, precision); + } + + public static void assertSame(final String message, final Object expected, final Object actual) { + assertTrue(String.format("%s: expected same objects: %s and %s", message, expected, actual), expected == actual); + } + + public static void checkAssert(final Class c) { + if (!c.desiredAssertionStatus()) { + throw error("You should enable assertions by running 'java -ea %s'", c.getName()); + } + } + + public static AssertionError error(final String format, final Object... args) { + final String message = String.format(format, args); + return args.length > 0 && args[args.length - 1] instanceof Throwable + ? new AssertionError(message, (Throwable) args[args.length - 1]) + : new AssertionError(message); + } + + public static void printStackTrace(final String message) { + new Exception(message).printStackTrace(System.out); + } +} diff --git a/common/base/BaseChecker.java b/common/base/BaseChecker.java new file mode 100644 index 0000000..67bd57c --- /dev/null +++ b/common/base/BaseChecker.java @@ -0,0 +1,20 @@ +package base; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public abstract class BaseChecker { + protected final TestCounter counter; + + protected BaseChecker(final TestCounter counter) { + this.counter = counter; + } + + public ExtendedRandom random() { + return counter.random(); + } + + public int mode() { + return counter.mode(); + } +} diff --git a/common/base/Either.java b/common/base/Either.java new file mode 100644 index 0000000..8a3eca8 --- /dev/null +++ b/common/base/Either.java @@ -0,0 +1,95 @@ +package base; + +import java.util.function.Function; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Either { + Either mapRight(final Function f); + Either flatMapRight(final Function> f); + T either(Function lf, Function rf); + + boolean isRight(); + + L getLeft(); + R getRight(); + + static Either right(final R value) { + return new Either<>() { + @Override + public Either mapRight(final Function f) { + return right(f.apply(value)); + } + + @Override + public Either flatMapRight(final Function> f) { + return f.apply(value); + } + + @Override + public T either(final Function lf, final Function rf) { + return rf.apply(value); + } + + @Override + public boolean isRight() { + return true; + } + + @Override + public L getLeft() { + return null; + } + + @Override + public R getRight() { + return value; + } + + @Override + public String toString() { + return String.format("Right(%s)", value); + } + }; + } + + static Either left(final L value) { + return new Either<>() { + @Override + public Either mapRight(final Function f) { + return left(value); + } + + @Override + public Either flatMapRight(final Function> f) { + return left(value); + } + + @Override + public T either(final Function lf, final Function rf) { + return lf.apply(value); + } + + @Override + public boolean isRight() { + return false; + } + + @Override + public L getLeft() { + return value; + } + + @Override + public R getRight() { + return null; + } + + @Override + public String toString() { + return String.format("Left(%s)", value); + } + }; + } +} diff --git a/common/base/ExtendedRandom.java b/common/base/ExtendedRandom.java new file mode 100644 index 0000000..ac2b059 --- /dev/null +++ b/common/base/ExtendedRandom.java @@ -0,0 +1,89 @@ +package base; + +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ExtendedRandom { + public static final String ENGLISH = "abcdefghijklmnopqrstuvwxyz"; + public static final String RUSSIAN = "абвгдеежзийклмнопрстуфхцчшщъыьэюя"; + public static final String GREEK = "αβγŋδεζηθικλμνξοπρτυφχψω"; + @SuppressWarnings("StaticMethodOnlyUsedInOneClass") + public static final String SPACES = " \t\n\u000B\u2029\f"; + + private final Random random; + + public ExtendedRandom(final Random random) { + this.random = random; + } + + public ExtendedRandom(final Class owner) { + this(new Random(7912736473497634913L + owner.getName().hashCode())); + } + + public String randomString(final String chars) { + return randomChar(chars) + (random.nextBoolean() ? "" : randomString(chars)); + } + + public char randomChar(final String chars) { + return chars.charAt(nextInt(chars.length())); + } + + public String randomString(final String chars, final int length) { + final StringBuilder string = new StringBuilder(); + for (int i = 0; i < length; i++) { + string.append(randomChar(chars)); + } + return string.toString(); + } + + public String randomString(final String chars, final int minLength, final int maxLength) { + return randomString(chars, nextInt(minLength, maxLength)); + } + + public boolean nextBoolean() { + return random.nextBoolean(); + } + + public int nextInt() { + return random.nextInt(); + } + + public int nextInt(final int min, final int max) { + return nextInt(max - min + 1) + min; + } + + public int nextInt(final int n) { + return random.nextInt(n); + } + + @SafeVarargs + public final T randomItem(final T... items) { + return items[nextInt(items.length)]; + } + + public T randomItem(final List items) { + return items.get(nextInt(items.size())); + } + + public Random getRandom() { + return random; + } + + public List random(final int list, final Function generator) { + return Stream.generate(() -> generator.apply(this)).limit(list).toList(); + } + + public double nextDouble() { + return random.nextDouble(); + } + + public void shuffle(final List all) { + Collections.shuffle(all, random); + } +} diff --git a/common/base/Functional.java b/common/base/Functional.java new file mode 100644 index 0000000..ef14dd5 --- /dev/null +++ b/common/base/Functional.java @@ -0,0 +1,92 @@ +package base; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Functional { + private Functional() {} + + public static List map(final Collection items, final Function f) { + return items.stream().map(f).collect(Collectors.toUnmodifiableList()); + } + + public static List map(final List items, final BiFunction f) { + return IntStream.range(0, items.size()) + .mapToObj(i -> f.apply(i, items.get(i))) + .collect(Collectors.toUnmodifiableList()); + } + + public static Map mapValues(final Map map, final Function f) { + return map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> f.apply(e.getValue()))); + } + + @SafeVarargs + public static Map mergeMaps(final Map... maps) { + return Stream.of(maps).flatMap(m -> m.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a)); + } + + @SafeVarargs + public static List concat(final Collection... items) { + final List result = new ArrayList<>(); + for (final Collection item : items) { + result.addAll(item); + } + return result; + } + + public static List append(final Collection collection, final T item) { + final List list = new ArrayList<>(collection); + list.add(item); + return list; + } + + public static List> allValues(final List vals, final int length) { + return Stream.generate(() -> vals) + .limit(length) + .reduce( + List.of(List.of()), + (prev, next) -> next.stream() + .flatMap(value -> prev.stream().map(list -> append(list, value))) + .toList(), + (prev, next) -> next.stream() + .flatMap(suffix -> prev.stream().map(prefix -> concat(prefix, suffix))) + .toList() + ); + } + + public static V get(final Map map, final K key) { + final V result = map.get(key); + if (result == null) { + throw new NullPointerException(key.toString() + " in " + map(map.keySet(), Objects::toString)); + } + return result; + } + + public static void addRange(final List values, final int d, final int c) { + for (int i = -d; i <= d; i++) { + values.add(c + i); + } + } + + public static void forEachPair(final T[] items, final BiConsumer consumer) { + assert items.length % 2 == 0; + IntStream.range(0, items.length / 2).forEach(i -> consumer.accept(items[i * 2], items[i * 2 + 1])); + } + + + public static List> toPairs(final T[] items) { + assert items.length % 2 == 0; + return IntStream.range(0, items.length / 2) + .mapToObj(i -> Pair.of(items[i * 2], items[i * 2 + 1])) + .toList(); + } +} diff --git a/common/base/Log.java b/common/base/Log.java new file mode 100644 index 0000000..00d9141 --- /dev/null +++ b/common/base/Log.java @@ -0,0 +1,56 @@ +package base; + +import java.util.function.Supplier; +import java.util.regex.Pattern; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Log { + private final Pattern LINES = Pattern.compile("\n"); + private int indent; + + public static Supplier action(final Runnable action) { + return () -> { + action.run(); + return null; + }; + } + + public void scope(final String name, final Runnable action) { + scope(name, action(action)); + } + + public T scope(final String name, final Supplier action) { + println(name); + indent++; + try { + return silentScope(name, action); + } finally { + indent--; + } + } + + public T silentScope(final String ignoredName, final Supplier action) { + return action.get(); + } + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + public void println(final Object value) { + for (final String line : LINES.split(String.valueOf(value))) { + System.out.println(indent() + line); + } + } + + public void format(final String format, final Object... args) { + println(String.format(format,args)); + } + + private String indent() { + return " ".repeat(indent); + } + + protected int getIndent() { + return indent; + } +} diff --git a/common/base/MainChecker.java b/common/base/MainChecker.java new file mode 100644 index 0000000..e526e7e --- /dev/null +++ b/common/base/MainChecker.java @@ -0,0 +1,28 @@ +package base; + +import java.util.List; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("StaticMethodOnlyUsedInOneClass") +public final class MainChecker { + private final Runner runner; + + public MainChecker(final Runner runner) { + this.runner = runner; + } + + public List run(final TestCounter counter, final String... input) { + return runner.run(counter, input); + } + + public List run(final TestCounter counter, final List input) { + return runner.run(counter, input); + } + + public void testEquals(final TestCounter counter, final List input, final List expected) { + runner.testEquals(counter, input, expected); + } + +} diff --git a/common/base/Named.java b/common/base/Named.java new file mode 100644 index 0000000..befb254 --- /dev/null +++ b/common/base/Named.java @@ -0,0 +1,15 @@ +package base; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public record Named(String name, T value) { + public static Named of(final String name, final T f) { + return new Named<>(name, f); + } + + @Override + public String toString() { + return name; + } +} diff --git a/common/base/Pair.java b/common/base/Pair.java new file mode 100644 index 0000000..8c27a31 --- /dev/null +++ b/common/base/Pair.java @@ -0,0 +1,44 @@ +package base; + +import java.util.Map; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings({"StaticMethodOnlyUsedInOneClass", "unused"}) +public record Pair(F first, S second) { + public static Pair of(final F first, final S second) { + return new Pair<>(first, second); + } + + public static Pair of(final Map.Entry e) { + return of(e.getKey(), e.getValue()); + } + + public static UnaryOperator> lift(final UnaryOperator f, final UnaryOperator s) { + return p -> of(f.apply(p.first), s.apply(p.second)); + } + + public static BinaryOperator> lift(final BinaryOperator f, final BinaryOperator s) { + return (p1, p2) -> of(f.apply(p1.first, p2.first), s.apply(p1.second, p2.second)); + } + + public static Function> tee( + final Function f, + final Function s + ) { + return t -> of(f.apply(t), s.apply(t)); + } + + @Override + public String toString() { + return "(" + first + ", " + second + ")"; + } + + public Pair second(final R second) { + return new Pair<>(first, second); + } +} diff --git a/common/base/Runner.java b/common/base/Runner.java new file mode 100644 index 0000000..7e30391 --- /dev/null +++ b/common/base/Runner.java @@ -0,0 +1,185 @@ +package base; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("unused") +@FunctionalInterface +public interface Runner { + List run(final TestCounter counter, final List input); + + default List run(final TestCounter counter, final String... input) { + return run(counter, List.of(input)); + } + + default void testEquals(final TestCounter counter, final List input, final List expected) { + counter.test(() -> { + final List actual = run(counter, input); + for (int i = 0; i < Math.min(expected.size(), actual.size()); i++) { + final String exp = expected.get(i); + final String act = actual.get(i); + if (!exp.equalsIgnoreCase(act)) { + Asserts.assertEquals("Line " + (i + 1), exp, act); + return; + } + } + Asserts.assertEquals("Number of lines", expected.size(), actual.size()); + }); + } + + static Packages packages(final String... packages) { + return new Packages(List.of(packages)); + } + + @FunctionalInterface + interface CommentRunner { + List run(String comment, TestCounter counter, List input); + } + + final class Packages { + private final List packages; + + private Packages(final List packages) { + this.packages = packages; + } + + public Runner std(final String className) { + final CommentRunner main = main(className); + return (counter, input) -> counter.call("io", () -> { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (final PrintWriter writer = new PrintWriter(baos)) { + input.forEach(writer::println); + } + + final InputStream oldIn = System.in; + try { + System.setIn(new ByteArrayInputStream(baos.toByteArray())); + return main.run(String.format("[%d input lines]", input.size()), counter, List.of()); + } finally { + System.setIn(oldIn); + } + }); + } + + @SuppressWarnings("ConfusingMainMethod") + public CommentRunner main(final String className) { + final Method method = findMain(className); + + return (comment, counter, input) -> { + counter.format("Running test %02d: java %s %s%n", counter.getTestNo(), method.getDeclaringClass().getName(), comment); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + @SuppressWarnings("UseOfSystemOutOrSystemErr") final PrintStream oldOut = System.out; + try { + System.setOut(new PrintStream(out, false, StandardCharsets.UTF_8)); + method.invoke(null, new Object[]{input.toArray(String[]::new)}); + final BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(out.toByteArray()), StandardCharsets.UTF_8)); + final List result = new ArrayList<>(); + while (true) { + final String line = reader.readLine(); + if (line == null) { + if (result.isEmpty()) { + result.add(""); + } + return result; + } + result.add(line.trim()); + } + } catch (final InvocationTargetException e) { + final Throwable cause = e.getCause(); + throw Asserts.error("main thrown exception %s: %s", cause.getClass().getSimpleName(), cause); + } catch (final Exception e) { + throw Asserts.error("Cannot invoke main: %s: %s", e.getClass().getSimpleName(), e.getMessage()); + } finally { + System.setOut(oldOut); + } + }; + } + + private Method findMain(final String className) { + try { + final URL url = new File(".").toURI().toURL(); + final List candidates = packages.stream() + .flatMap(pkg -> { + final String prefix = pkg.isEmpty() ? pkg : pkg + "."; + return Stream.of(prefix + className, prefix + "$" + className); + }) + .toList(); + + //noinspection ClassLoaderInstantiation,resource,IOResourceOpenedButNotSafelyClosed + final URLClassLoader classLoader = new URLClassLoader(new URL[]{url}); + for (final String candidate : candidates) { + try { + final Class loaded = classLoader.loadClass(candidate); + if (!Modifier.isPublic(loaded.getModifiers())) { + throw Asserts.error("Class %s is not public", candidate); + } + final Method main = loaded.getMethod("main", String[].class); + if (!Modifier.isPublic(main.getModifiers()) || !Modifier.isStatic(main.getModifiers())) { + throw Asserts.error("Method main of class %s should be public and static", candidate); + } + return main; + } catch (final ClassNotFoundException e) { + // Ignore + } catch (final NoSuchMethodException e) { + throw Asserts.error("Could not find method main(String[]) in class %s", candidate, e); + } + } + throw Asserts.error("Could not find neither of classes %s", candidates); + } catch (final MalformedURLException e) { + throw Asserts.error("Invalid path", e); + } + } + + private static String getClassName(final String pkg, final String className) { + return pkg.isEmpty() ? className : pkg + "." + className; + } + + public Runner args(final String className) { + final CommentRunner main = main(className); +// final AtomicReference prev = new AtomicReference<>(""); + return (counter, input) -> { + final int total = input.stream().mapToInt(String::length).sum() + input.size() * 3; + final String comment = total <= 300 + ? input.stream().collect(Collectors.joining("\" \"", "\"", "\"")) + : input.size() <= 100 + ? String.format("[%d arguments, sizes: %s]", input.size(), input.stream() + .mapToInt(String::length) + .mapToObj(String::valueOf) + .collect(Collectors.joining(", "))) + : String.format("[%d arguments, total size: %d]", input.size(), total); +// assert comment.length() <= 5 || !prev.get().equals(comment) : "Duplicate tests " + comment; +// prev.set(comment); + return main.run(comment, counter, input); + }; + } + + public Runner files(final String className) { + final Runner args = args(className); + return (counter, input) -> counter.call("io", () -> { + final Path inf = counter.getFile("in"); + final Path ouf = counter.getFile("out"); + Files.write(inf, input); + args.run(counter, List.of(inf.toString(), ouf.toString())); + final List output = Files.readAllLines(ouf); + Files.delete(inf); + Files.delete(ouf); + return output; + }); + } + } +} diff --git a/common/base/Selector.java b/common/base/Selector.java new file mode 100644 index 0000000..dc119b9 --- /dev/null +++ b/common/base/Selector.java @@ -0,0 +1,143 @@ +package base; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Selector { + private final Class owner; + private final List modes; + private final Set variantNames = new LinkedHashSet<>(); + private final Map> variants = new LinkedHashMap<>(); + + public Selector(final Class owner, final String... modes) { + this.owner = owner; + this.modes = List.of(modes); + } + + public Selector variant(final String name, final Consumer operations) { + Asserts.assertTrue("Duplicate variant " + name, variants.put(name.toLowerCase(), operations) == null); + variantNames.add(name); + return this; + } + + private static void check(final boolean condition, final String format, final Object... args) { + if (!condition) { + throw new IllegalArgumentException(String.format(format, args)); + } + } + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + public void main(final String... args) { + try { + final String mode; + if (modes.isEmpty()) { + check(args.length >= 1, "At least one argument expected, found %s", args.length); + mode = ""; + } else { + check(args.length >= 2, "At least two arguments expected, found %s", args.length); + mode = args[0]; + } + + final List vars = Arrays.stream(args).skip(modes.isEmpty() ? 0 : 1) + .flatMap(arg -> Arrays.stream(arg.split("[ +]+"))) + .toList(); + + test(mode, vars); + } catch (final IllegalArgumentException e) { + System.err.println("ERROR: " + e.getMessage()); + if (modes.isEmpty()) { + System.err.println("Usage: " + owner.getName() + " VARIANT..."); + } else { + System.err.println("Usage: " + owner.getName() + " MODE VARIANT..."); + System.err.println("Modes: " + String.join(", ", modes)); + } + System.err.println("Variants: " + String.join(", ", variantNames)); + System.exit(1); + } + } + + public void test(final String mode, List vars) { + final int modeNo = modes.isEmpty() ? -1 : modes.indexOf(mode) ; + check(modes.isEmpty() || modeNo >= 0, "Unknown mode '%s'", mode); + if (variantNames.contains("Base") && !vars.contains("Base")) { + vars = new ArrayList<>(vars); + vars.add(0, "Base"); + } + + vars.forEach(variant -> check(variants.containsKey(variant.toLowerCase()), "Unknown variant '%s'", variant)); + + final Map properties = modes.isEmpty() + ? Map.of("variant", String.join("+", vars)) + : Map.of("variant", String.join("+", vars), "mode", mode); + final TestCounter counter = new TestCounter(owner, modeNo, properties); + counter.printHead(); + vars.forEach(variant -> counter.scope("Testing " + variant, () -> variants.get(variant.toLowerCase()).accept(counter))); + counter.printStatus(); + } + + public static Composite composite(final Class owner, final Function factory, final String... modes) { + return new Composite<>(owner, factory, (tester, counter) -> tester.test(), modes); + } + + public static Composite composite(final Class owner, final Function factory, final BiConsumer tester, final String... modes) { + return new Composite<>(owner, factory, tester, modes); + } + + public List getModes() { + return modes.isEmpty() ? List.of("~") : modes; + } + + public List getVariants() { + return List.copyOf(variants.keySet()); + } + + /** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ + public static final class Composite { + private final Selector selector; + private final Function factory; + private final BiConsumer tester; + private List> base; + + private Composite(final Class owner, final Function factory, final BiConsumer tester, final String... modes) { + selector = new Selector(owner, modes); + this.factory = factory; + this.tester = tester; + } + + @SafeVarargs + public final Composite variant(final String name, final Consumer... parts) { + if ("Base".equalsIgnoreCase(name)) { + base = List.of(parts); + return v(name.toLowerCase()); + } else { + return v(name, parts); + } + } + + @SafeVarargs + private Composite v(final String name, final Consumer... parts) { + selector.variant(name, counter -> { + final V variant = factory.apply(counter); + for (final Consumer part : base) { + part.accept(variant); + } + for (final Consumer part : parts) { + part.accept(variant); + } + tester.accept(variant, counter); + }); + return this; + } + + public Selector selector() { + return selector; + } + } +} diff --git a/common/base/TestCounter.java b/common/base/TestCounter.java new file mode 100644 index 0000000..85fb9c9 --- /dev/null +++ b/common/base/TestCounter.java @@ -0,0 +1,184 @@ +package base; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class TestCounter extends Log { + public static final int DENOMINATOR = Integer.getInteger("base.denominator", 1); + public static final int DENOMINATOR2 = (int) Math.round(Math.sqrt(DENOMINATOR)); + + private static final String JAR_EXT = ".jar"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); + + private final Class owner; + private final int mode; + private final Map properties; + private final ExtendedRandom random; + + private final long start = System.currentTimeMillis(); + private int passed; + + public TestCounter(final Class owner, final int mode, final Map properties) { + Locale.setDefault(Locale.US); + Asserts.checkAssert(getClass()); + + this.owner = owner; + this.mode = mode; + this.properties = properties; + random = new ExtendedRandom(owner); + } + + public int mode() { + return mode; + } + + public int getTestNo() { + return passed + 1; + } + + public void test(final Runnable action) { + testV(() -> { + action.run(); + return null; + }); + } + + public void testForEach(final Iterable items, final Consumer action) { + for (final T item : items) { + test(() -> action.accept(item)); + } + } + + public T testV(final Supplier action) { + return silentScope("Test " + getTestNo(), () -> { + final T result = action.get(); + passed++; + return result; + }); + } + + private String getLine() { + return getIndent() == 0 ? "=" : "-"; + } + + public void printHead() { + println("=== " + getTitle()); + } + + public void printStatus() { + format("%s%n%s%n", getLine().repeat(30), getTitle()); + format("%d tests passed in %dms%n", passed, System.currentTimeMillis() - start); + println("Version: " + getVersion(owner)); + println(""); + } + + private String getTitle() { + return String.format("%s %s", owner.getSimpleName(), properties.isEmpty() ? "" : properties); + } + + private static String getVersion(final Class clazz) { + try { + final ClassLoader cl = clazz.getClassLoader(); + final URL url = cl.getResource(clazz.getName().replace('.', '/') + ".class"); + if (url == null) { + return "(no manifest)"; + } + + final String path = url.getPath(); + final int index = path.indexOf(JAR_EXT); + if (index == -1) { + return DATE_FORMAT.format(new Date(new File(path).lastModified())); + } + + final String jarPath = path.substring(0, index + JAR_EXT.length()); + try (final JarFile jarFile = new JarFile(new File(new URI(jarPath)))) { + final JarEntry entry = jarFile.getJarEntry("META-INF/MANIFEST.MF"); + return DATE_FORMAT.format(new Date(entry.getTime())); + } + } catch (final IOException | URISyntaxException e) { + return "error: " + e; + } + } + + public T call(final String message, final SupplierE supplier) { + return get(supplier).either(e -> fail(e, "%s", message), Function.identity()); + } + + public void shouldFail(final String message, @SuppressWarnings("TypeMayBeWeakened") final RunnableE action) { + test(() -> get(action).either(e -> null, v -> fail("%s", message))); + } + + public T fail(final String format, final Object... args) { + return fail(Asserts.error(format, args)); + } + + public T fail(final Throwable throwable) { + return fail(throwable, "%s: %s", throwable.getClass().getSimpleName(), throwable.getMessage()); + } + + public T fail(final Throwable throwable, final String format, final Object... args) { + final String message = String.format(format, args); + println("ERROR: " + message); + throw throwable instanceof Error ? (Error) throwable : new AssertionError(throwable); + } + + public void checkTrue(final boolean condition, final String message, final Object... args) { + if (!condition) { + fail(message, args); + } + } + + public static Either get(final SupplierE supplier) { + return supplier.get(); + } + + public Path getFile(final String suffix) { + return Paths.get(String.format("test%d.%s", getTestNo(), suffix)); + } + + public ExtendedRandom random() { + return random; + } + + @FunctionalInterface + public interface SupplierE extends Supplier> { + T getE() throws Exception; + + @Override + default Either get() { + try { + return Either.right(getE()); + } catch (final Exception e) { + return Either.left(e); + } + } + } + + @FunctionalInterface + public interface RunnableE extends SupplierE { + void run() throws Exception; + + @Override + default Void getE() throws Exception { + run(); + return null; + } + } +} diff --git a/common/base/Tester.java b/common/base/Tester.java new file mode 100644 index 0000000..d30260d --- /dev/null +++ b/common/base/Tester.java @@ -0,0 +1,18 @@ +package base; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public abstract class Tester extends BaseChecker { + protected Tester(final TestCounter counter) { + super(counter); + } + + public abstract void test(); + + public void run(final Class test, final String... args) { + System.out.println("=== Testing " + test.getSimpleName() + " " + String.join(" ", args)); + test(); + counter.printStatus(); + } +} diff --git a/common/base/Unit.java b/common/base/Unit.java new file mode 100644 index 0000000..290febf --- /dev/null +++ b/common/base/Unit.java @@ -0,0 +1,15 @@ +package base; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Unit { + public static final Unit INSTANCE = new Unit(); + + private Unit() { } + + @Override + public String toString() { + return "unit"; + } +} diff --git a/common/base/package-info.java b/common/base/package-info.java new file mode 100644 index 0000000..9ae2b5f --- /dev/null +++ b/common/base/package-info.java @@ -0,0 +1,7 @@ +/** + * Common homeworks test classes + * of Paradigms of Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package base; \ No newline at end of file diff --git a/common/common/Engine.java b/common/common/Engine.java new file mode 100644 index 0000000..bb0b8c1 --- /dev/null +++ b/common/common/Engine.java @@ -0,0 +1,37 @@ +package common; + +import base.Asserts; + +import java.util.function.BiFunction; + +/** + * Test engine. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Engine { + Result prepare(String expression); + + Result evaluate(final Result prepared, double[] vars); + + Result toString(final Result prepared); + + default Result parse(final String expression) { + throw new UnsupportedOperationException(); + } + + record Result(String context, T value) { + public void assertEquals(final T expected) { + Asserts.assertEquals(context(), expected, value()); + } + + public Result cast(final BiFunction convert) { + return new Result<>(context(), convert.apply(value(), context())); + } + + @Override + public String toString() { + return context(); + } + } +} diff --git a/common/common/EngineException.java b/common/common/EngineException.java new file mode 100644 index 0000000..ffa0b68 --- /dev/null +++ b/common/common/EngineException.java @@ -0,0 +1,12 @@ +package common; + +/** + * Thrown on test engine error. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class EngineException extends RuntimeException { + public EngineException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/common/common/expression/AnyOp.java b/common/common/expression/AnyOp.java new file mode 100644 index 0000000..8262d35 --- /dev/null +++ b/common/common/expression/AnyOp.java @@ -0,0 +1,13 @@ +package common.expression; + +import java.util.Collection; +import java.util.function.Predicate; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public record AnyOp(ExprTester.Func f, int min, int max, int fixed) { + public Predicate> arity() { + return args -> min <= args.size() && args.size() <= max; + } +} diff --git a/common/common/expression/ArithmeticBuilder.java b/common/common/expression/ArithmeticBuilder.java new file mode 100644 index 0000000..dca3b0c --- /dev/null +++ b/common/common/expression/ArithmeticBuilder.java @@ -0,0 +1,271 @@ +package common.expression; + +import java.util.List; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Basic arithmetics. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ArithmeticBuilder implements OperationsBuilder { + private final BaseVariant variant; + private final F neg; + private final F add; + private final F sub; + private final F mul; + private final F div; + + public ArithmeticBuilder(final boolean varargs, final List variables) { + variant = new BaseVariant(varargs); + variables.forEach(this::variable); + + //noinspection Convert2MethodRef + variant.infix("+", 100, (a, b) -> a + b); + variant.infix("-", 100, (a, b) -> a - b); + variant.infix("*", 200, (a, b) -> a * b); + variant.infix("/", 200, (a, b) -> a / b); + variant.unary("negate", a -> -a); + + add = f("+", 2); + sub = f("-", 2); + mul = f("*", 2); + div = f("/", 2); + neg = f("negate", 1); + + basicTests(); + } + + public void basicTests() { + final List ops = List.of(neg, add, sub, mul, div); + variant.tests(() -> Stream.of( + Stream.of(variant.c()), + variant.getVariables().stream(), + ops.stream().map(F::c), + ops.stream().map(F::v), + ops.stream().map(F::r), + ops.stream().map(F::r), + Stream.of( + div.f(neg.r(), r()), + div.f(r(), mul.r()), + add.f(add.f(mul.r(), mul.r()), mul.r()), + sub.f(add.f(mul.r(), mul.f(r(), mul.f(r(), mul.r()))), mul.r()) + ) + ).flatMap(Function.identity())); + } + + @Override + public void constant(final String name, final String alias, final double value) { + alias(name, alias); + final ExprTester.Func expr = vars -> value; + variant.nullary(name, expr); + final Expr constant = Expr.nullary(name, expr); + variant.tests(() -> Stream.of( + neg.f(constant), + add.f(constant, r()), + sub.f(r(), constant), + mul.f(r(), constant), + div.f(constant, r()) + )); + } + + @Override + public void unary(final String name, final String alias, final DoubleUnaryOperator op) { + variant.unary(name, op); + variant.alias(name, alias); + unaryTests(name); + } + + private void unaryTests(final String name) { + final F op = f(name, 1); + variant.tests(() -> Stream.of( + op.c(), + op.v(), + op.f(sub.r()), + op.f(add.r()), + op.f(div.f(op.r(), add.r())), + add.f(op.f(op.f(add.r())), mul.f(r(), mul.f(r(), op.r()))) + )); + } + + @Override + public void binary(final String name, final String alias, final DoubleBinaryOperator op) { + variant.binary(name, op); + variant.alias(name, alias); + binaryTests(name); + } + + private void binaryTests(final String name) { + final F op = f(name, 2); + variant.tests(() -> Stream.of( + op.c(), + op.v(), + op.r(), + op.f(neg.r(), add.r()), + op.f(sub.r(), neg.r()), + op.f(neg.r(), op.r()), + op.f(op.r(), neg.r()) + )); + } + + private record F(String name, int arity, BaseVariant variant) { + public Expr f(final Expr... args) { + assert arity < 0 || arity == args.length; + return variant.f(name, args); + } + + public Expr v() { + return g(variant::v); + } + + public Expr c() { + return g(variant::c); + } + + public Expr r() { + return g(variant::r); + } + + private Expr g(final Supplier g) { + return f(Stream.generate(g).limit(arity).toArray(Expr[]::new)); + } + } + + private F f(final String name, final int arity) { + return new F(name, arity, variant); + } + + private Expr r() { + return variant.r(); + } + + private Expr f(final String name, final Expr... args) { + return variant.f(name, args); + } + + @Override + public void infix(final String name, final String alias, final int priority, final DoubleBinaryOperator op) { + variant.infix(name, priority, op); + variant.alias(name, alias); + binaryTests(name); + } + + + @Override + public void fixed( + final String name, + final String alias, + final int arity, + final ExprTester.Func f + ) { + variant.fixed(name, arity, f); + variant.alias(name, alias); + + if (arity == 1) { + unaryTests(name); + } else if (arity == 2) { + binaryTests(name); + } else if (arity == 3) { + final F op = f(name, 3); + variant.tests(() -> { + final Expr e1 = op.c(); + final Expr e2 = op.v(); + final Expr e3 = op.f(add.r(), sub.r(), mul.r()); + return Stream.of( + op.f(variant.c(), r(), r()), + op.f(r(), variant.c(), r()), + op.f(r(), r(), variant.c()), + op.f(variant.v(), mul.v(), mul.v()), + op.f(mul.v(), variant.v(), mul.v()), + op.f(mul.v(), r(), mul.v()), + op.r(), + e1, + e2, + e3, + op.f(e1, e2, e3) + ); + }); + } else if (arity == 4) { + final F op = f(name, 4); + variant.tests(() -> { + final Expr e1 = op.c(); + final Expr e2 = op.v(); + final Expr e3 = op.r(); + final Expr e4 = op.f(add.r(), sub.r(), mul.r(), div.r()); + return Stream.of( + op.r(), + op.r(), + op.r(), + e1, + e2, + e3, + e4, + op.f(e1, e2, e3, e4) + ); + }); + } else { + variant.tests(() -> Stream.concat( + Stream.of( + f(name, arity, variant::c), + f(name, arity, variant::v) + ), + IntStream.range(0, 10).mapToObj(i -> f(name, arity, variant::r)) + )); + } + } + + private Expr f(final String name, final int arity, final Supplier generator) { + return f(name, Stream.generate(generator).limit(arity).toArray(Expr[]::new)); + } + + @Override + public void any( + final String name, + final String alias, + final AnyOp op + ) { + variant.any(name, op); + variant.alias(name, alias); + + if (variant.hasVarargs()) { + variant.tests(() -> Stream.>of( + List.of(), + List.of(r()), + List.of(r(), r()), + List.of(r(), r(), r()), + List.of(r(), r(), r(), r()), + List.of(r(), r(), r(), r(), r()), + List.of(add.r(), r()), + List.of(r(), r(), sub.r()) + ).filter(op.arity()).map(args -> args.toArray(Expr[]::new)).map(f(name, -1)::f)); + } + + variant.tests(() -> IntStream.rangeClosed(op.min(), op.max()) + .mapToObj(i -> f(name, variant.hasVarargs() ? i : op.fixed(), variant::r))); + } + + @Override + public void variable(final String name) { + variant.variable(name, variant.getVariables().size()); + } + + @Override + public void alias(final String name, final String alias) { + variant.alias(name, alias); + } + + @Override + public void remove(final String... names) { + variant.remove(names); + } + + @Override + public BaseVariant variant() { + return variant; + } +} diff --git a/common/common/expression/BaseVariant.java b/common/common/expression/BaseVariant.java new file mode 100644 index 0000000..9e2e5b7 --- /dev/null +++ b/common/common/expression/BaseVariant.java @@ -0,0 +1,211 @@ +package common.expression; + +import base.ExtendedRandom; +import common.expression.ExprTester.Func; + +import java.util.*; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * Base expressions variant. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class BaseVariant implements Variant { + private static final int MAX_C = 1_000; + private static final Expr ZERO = c(0); + + private final ExtendedRandom random = new ExtendedRandom(getClass()); + private final boolean varargs; + + private final StringMap operators = new StringMap<>(); + private final StringMap nullary = new StringMap<>(); + private final StringMap variables = new StringMap<>(); + private final Map aliases = new HashMap<>(); + + private final Map priorities = new HashMap<>(); + + public final List>> tests = new ArrayList<>(); + + public BaseVariant(final boolean varargs) { + this.varargs = varargs; + } + + public List getTests() { + return tests.stream().flatMap(Supplier::get).toList(); + } + + public Expr randomTest(final int size) { + return generate(size / 10 + 2); + } + + private Expr generate(final int depth) { + return depth > 0 ? generateOp(depth) : r(); + } + + public Expr r() { + if (random.nextBoolean()) { + return variables.random(random); + } else if (nullary.isEmpty() || random.nextBoolean()){ + return c(); + } else { + return nullary.random(random); + } + } + + public Expr c() { + return random.nextBoolean() ? ZERO : c(random.nextInt(-MAX_C, MAX_C)); + } + + public Expr v() { + return random().randomItem(variables.values().toArray(Expr[]::new)); + } + + protected Expr generateOp(final int depth) { + if (random.nextInt(6) == 0 || operators.isEmpty()) { + return generateP(depth); + } else { + final Operator operator = operators.random(random); + final Expr[] args = Stream.generate(() -> generateP(depth)) + .limit(random.nextInt(operator.minArity, operator.maxArity)) + .toArray(Expr[]::new); + return f(operator.name, args); + } + } + + protected Expr generateP(final int depth) { + return generate(random.nextInt(depth)); + } + + public void tests(final Supplier> tests) { + this.tests.add(tests); + } + + public void fixed(final String name, final int arity, final Func f) { + op(name, arity, arity, f); + } + + public void op(final String name, final int minArity, final int maxArity, final Func f) { + assert !operators.contains(name) : "Duplicate op %s".formatted(name); + operators.put(name, new Operator(name, minArity, maxArity, f)); + } + + public void remove(final String... names) { + //noinspection SlowAbstractSetRemoveAll + operators.values.keySet().removeAll(Arrays.asList(names)); + } + + public void any(final String name, final AnyOp op) { + if (varargs) { + op(name, op.min(), op.max(), op.f()); + } else { + op(name, op.fixed(), op.fixed(), op.f()); + } + } + + public void unary(final String name, final DoubleUnaryOperator answer) { + fixed(name, 1, args -> answer.applyAsDouble(args[0])); + } + + public void binary(final String name, final DoubleBinaryOperator answer) { + fixed(name, 2, args -> answer.applyAsDouble(args[0], args[1])); + } + + public void infix(final String name, final int priority, final DoubleBinaryOperator answer) { + binary(name, answer); + priorities.put(name, priority); + } + + public void nullary(final String name, final Func f) { + nullary.put(name, Expr.nullary(name, f)); + } + + public Expr f(final String name, final Expr... args) { + return Expr.f(name, operators.get(name), List.of(args)); + } + + protected Expr n(final String name) { + return nullary.get(name); + } + + public static Expr c(final int value) { + return Expr.constant(value); + } + + public Expr variable(final String name, final int index) { + final Expr variable = Expr.variable(name, index); + variables.put(name, variable); + return variable; + } + + public List getVariables() { + return List.copyOf(variables.values()); + } + + @Override + public ExtendedRandom random() { + return random; + } + + @Override + public boolean hasVarargs() { + return varargs; + } + + @Override + public Integer getPriority(final String op) { + return priorities.get(op); + } + + private record Operator(String name, int minArity, int maxArity, Func f) implements Func { + private Operator { + assert 0 <= minArity && minArity <= maxArity; + } + + @Override + public double applyAsDouble(final double[] args) { + return Arrays.stream(args).allMatch(Double::isFinite) ? f.applyAsDouble(args) : Double.NaN; + } + } + + private static class StringMap { + private final List names = new ArrayList<>(); + private final Map values = new HashMap<>(); + + public T get(final String name) { + return values.get(name); + } + + public T random(final ExtendedRandom random) { + return get(random.randomItem(names)); + } + + private boolean isEmpty() { + return values.isEmpty(); + } + + private void put(final String name, final T value) { + names.add(name); + values.put(name, value); + } + + private Collection values() { + return values.values(); + } + + public boolean contains(final String name) { + return values.containsKey(name); + } + } + + public void alias(final String name, final String alias) { + aliases.put(name, alias); + } + + public String resolve(final String alias) { + return aliases.getOrDefault(alias, alias); + } +} diff --git a/common/common/expression/Dialect.java b/common/common/expression/Dialect.java new file mode 100644 index 0000000..d0b7603 --- /dev/null +++ b/common/common/expression/Dialect.java @@ -0,0 +1,57 @@ +package common.expression; + +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * Expression dialect. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Dialect { + private final Expr.Cata cata; + + private Dialect(final Expr.Cata cata) { + this.cata = cata; + } + + public Dialect(final String variable, final String constant, final BiFunction, String> nary) { + this(new Expr.Cata<>(variable::formatted, constant::formatted, name -> name, nary)); + } + + public Dialect(final String variable, final String constant, final String operation, final String separator) { + this(variable, constant, operation(operation, separator)); + } + + public static BiFunction, String> operation(final String template, final String separator) { + return (op, args) -> template.replace("{op}", op).replace("{args}", String.join(separator, args)); + } + + public Dialect renamed(final Function renamer) { + return updated(cata -> cata.withOperation(nary -> (name, args) -> nary.apply(renamer.apply(name), args))); + } + + public Dialect updated(final UnaryOperator> updater) { + return new Dialect(updater.apply(cata)); + } + + public String render(final Expr expr) { + return expr.cata(cata); + } + + public String meta(final String name, final String... args) { + return cata.operation(name, List.of(args)); + } + + public Dialect functional() { + return renamed(Dialect::toFunctional); + } + + private static String toFunctional(final String name) { + return name.chars().allMatch(Character::isUpperCase) + ? name.toLowerCase() + : Character.toLowerCase(name.charAt(0)) + name.substring(1); + } +} diff --git a/common/common/expression/Diff.java b/common/common/expression/Diff.java new file mode 100644 index 0000000..12e7ca0 --- /dev/null +++ b/common/common/expression/Diff.java @@ -0,0 +1,157 @@ +package common.expression; + +import base.Asserts; +import base.Named; +import common.Engine; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static common.expression.ExprTester.EPS; +import static common.expression.ExprTester.Test; + +/** + * Expression differentiator. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Diff { + private static final double D = 1e-6; + + private final int base; + private final Dialect dialect; + + public Diff(final int base, final Dialect dialect) { + this.dialect = dialect; + this.base = base; + } + + public void diff(final ExprTester tester, final boolean reparse) { + tester.addStage(() -> { + for (final Test expr : tester.language.getTests()) { + checkDiff(tester, expr, reparse, false); + } + }); + } + + private List> checkDiff( + final ExprTester tester, + final Test test, + final boolean reparse, + final boolean simplify + ) { + final List> results = new ArrayList<>(test.variables().size() + 1); + System.out.println(" Testing diff: " + test.parsed()); + + if (simplify) { + final Engine.Result simplified = tester.engine.prepare(dialect.meta("simplify", test.parsed())); + test.points().forEachOrdered(point -> { + final double[] vars = Arrays.stream(point).map(v -> v + base).toArray(); + tester.assertValue("simplified expression", simplified, vars, test.evaluate(vars)); + }); + results.add(tester.engine.toString(simplified)); + } + + final double[] indices = IntStream.range(0, test.variables().size()).mapToDouble(a -> a).toArray(); + for (final Expr variable : test.variables()) { + final List>> ways = new ArrayList<>(); + final String diffS = dialect.meta("diff", test.parsed(), dialect.render(variable)); + addWays("diff", tester, reparse, diffS, ways); + + if (simplify) { + final String simplifyS = dialect.meta("simplify", diffS); + results.add(tester.engine.toString(addWays("simplified", tester, reparse, simplifyS, ways))); + } + + final int index = (int) variable.evaluate(indices); + + test.points().forEachOrdered(point -> { + final double[] vars = Arrays.stream(point).map(v -> v + base).toArray(); + final double center = test.evaluate(vars); + if (ok(center)) { + final double lft = evaluate(test, vars, index, -D); + final double rt = evaluate(test, vars, index, D); + final double left = (center - lft) / D; + final double right = (rt - center) / D; + if (ok(lft) && ok(rt) && ok(left) && ok(right) && Math.abs(left - right) < EPS) { + for (final Named> way : ways) { + tester.assertValue( + "diff by %s, %s".formatted(dialect.render(variable), way.name()), + way.value(), vars, (left + right) / 2 + ); + } + } + } + }); + } + return results; + } + + private static Engine.Result addWays( + final String name, + final ExprTester tester, + final boolean reparse, + final String exprS, + final List>> ways + ) { + final Engine.Result exprR = tester.engine.prepare(exprS); + ways.add(Named.of(name, exprR)); + if (reparse) { + ways.add(Named.of("reparsed " + name, tester.parse(tester.engine.toString(exprR).value()))); + } + return exprR; + } + + private static boolean ok(final double value) { + final double abs = Math.abs(value); + return EPS < abs && abs < 1 / EPS; + } + + private static double evaluate(final Test test, final double[] vars, final int index, final double d) { + vars[index] += d; + final double result = test.evaluate(vars); + vars[index] -= d; + return result; + } + + public void simplify(final ExprTester tester) { + final List simplifications = tester.language.getSimplifications(); + if (simplifications == null) { + return; + } + + tester.addStage(() -> { + final List newSimplifications = new ArrayList<>(); + final List tests = tester.language.getTests(); + + for (int i = 0; i < simplifications.size(); i++) { + final Test expr = tests.get(i); + final int[] expected = simplifications.get(i); + final List> actual = checkDiff(tester, expr, true, true); + if (expected != null) { + for (int j = 0; j < expected.length; j++) { + final Engine.Result result = actual.get(j); + final int length = result.value().length(); + Asserts.assertTrue( + "Simplified length too long: %d instead of %d%s" + .formatted(length, expected[j], result.context()), + length <= expected[j] + ); + } + } else { + newSimplifications.add(actual.stream().mapToInt(result -> result.value().length()).toArray()); + } + } + if (!newSimplifications.isEmpty()) { + System.err.println(newSimplifications.stream() + .map(row -> Arrays.stream(row) + .mapToObj(Integer::toString) + .collect(Collectors.joining(", ", "{", "}"))) + .collect(Collectors.joining(", ", "new int[][]{", "}"))); + System.err.println(simplifications.size() + " " + newSimplifications.size()); + throw new AssertionError("Uncovered"); + } + }); + } +} diff --git a/common/common/expression/Expr.java b/common/common/expression/Expr.java new file mode 100644 index 0000000..308fe22 --- /dev/null +++ b/common/common/expression/Expr.java @@ -0,0 +1,89 @@ +package common.expression; + +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.UnaryOperator; + +/** + * Expression instance. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Expr { + private final ExprTester.Func answer; + /* There are no forall generics in Java, so using Object as placeholder. */ + private final Function, Object> coCata; + + private Expr(final ExprTester.Func answer, final Function, Object> coCata) { + this.answer = answer; + this.coCata = coCata; + } + + public T cata(final Cata cata) { + @SuppressWarnings("unchecked") + final Function, T> coCata = (Function, T>) (Function) this.coCata; + return coCata.apply(cata); + } + + public double evaluate(final double... vars) { + return answer.applyAsDouble(vars); + } + + static Expr f(final String name, final ExprTester.Func operator, final List args) { + Objects.requireNonNull(operator, "Unknown operation " + name); + return new Expr( + vars -> operator.applyAsDouble(args.stream().mapToDouble(arg -> arg.evaluate(vars)).toArray()), + cata -> cata.operation( + name, + args.stream().map(arg -> arg.cata(cata)).toList() + ) + ); + } + + static Expr constant(final int value) { + return new Expr(vars -> value, cata -> cata.constant(value)); + } + + static Expr variable(final String name, final int index) { + return new Expr(vars -> vars[index], cata -> cata.variable(name)); + } + + static Expr nullary(final String name, final ExprTester.Func answer) { + return new Expr(answer, cata -> cata.nullary(name)); + } + + /** + * Expression catamorphism. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ + public record Cata( + Function variable, + IntFunction constant, + Function nullary, + BiFunction, T> operation + ) { + public T variable(final String name) { + return variable.apply(name); + } + + public T constant(final int value) { + return constant.apply(value); + } + + public T nullary(final String name) { + return nullary.apply(name); + } + + public T operation(final String name, final List args) { + return operation.apply(name, args); + } + + public Cata withOperation(final UnaryOperator, T>> updater) { + return new Cata<>(variable, constant, nullary, updater.apply(operation)); + } + } +} diff --git a/common/common/expression/ExprTester.java b/common/common/expression/ExprTester.java new file mode 100644 index 0000000..e07f777 --- /dev/null +++ b/common/common/expression/ExprTester.java @@ -0,0 +1,221 @@ +package common.expression; + +import base.Asserts; +import base.ExtendedRandom; +import base.TestCounter; +import base.Tester; +import common.Engine; +import common.EngineException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.ToDoubleFunction; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Expressions tester. + * + * @author Niyaz Nigmatullin + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ExprTester extends Tester { + public static final int N = 128; + public static final double EPS = 1e-3; + public static final int RANDOM_TESTS = 444; + + private final int randomTests; + /*package*/ final Engine engine; + /*package*/ final Language language; + private final List stages = new ArrayList<>(); + private final boolean testToString; + + private final Generator spoiler; + private final Generator corruptor; + + public static final Generator STANDARD_SPOILER = (input, expr, random, builder) -> builder + .add(input) + .add(addSpaces(input, random)); + + public ExprTester( + final TestCounter counter, + final int randomTests, + final Engine engine, + final Language language, + final boolean testToString, + final Generator spoiler, + final Generator corruptor + ) { + super(counter); + this.randomTests = randomTests; + this.engine = engine; + this.language = language; + this.testToString = testToString; + this.spoiler = spoiler; + this.corruptor = corruptor; + } + + private static final Predicate UNSAFE = Pattern.compile("[-\\p{Alnum}+*/.=&|^<>◀▶◁▷≤≥?⁰-⁹₀-₉:]").asPredicate(); + + private static boolean safe(final char ch) { + return !UNSAFE.test("" + ch); + } + + public static String addSpaces(final String expression, final ExtendedRandom random) { + String spaced = expression; + for (int n = StrictMath.min(10, 200 / expression.length()); n > 0;) { + final int index = random.nextInt(spaced.length() + 1); + final char c = index == 0 ? 0 : spaced.charAt(index - 1); + final char nc = index == spaced.length() ? 0 : spaced.charAt(index); + if ((safe(c) || safe(nc)) && c != '\'' && nc != '\'' && c != '"' && nc != '"') { + spaced = spaced.substring(0, index) + " " + spaced.substring(index); + n--; + } + } + return spaced; + } + + @Override + public void test() { + for (final Test test : language.getTests()) { + try { + test(test, prepared -> counter.scope( + "Testing: " + prepared, + () -> test.points().forEachOrdered(vars -> assertValue( + "original expression", + prepared, + vars, + test.evaluate(vars) + ))) + ); + } catch (final RuntimeException | AssertionError e) { + throw new AssertionError("Error while testing " + test.parsed() + ": " + e.getMessage(), e); + } + } + + counter.scope("Random tests", () -> testRandom(randomTests)); + stages.forEach(Runnable::run); + } + + public static int limit(final int variables) { + return (int) Math.floor(Math.pow(N, 1.0 / variables)); + } + + private void test(final Test test, final Consumer> check) { + final Consumer> fullCheck = parsed -> counter.test(() -> { + check.accept(parsed); + if (testToString) { + counter.test(() -> engine.toString(parsed).assertEquals(test.toStr())); + } + }); + fullCheck.accept(engine.prepare(test.parsed())); + spoiler.forEach(10, test, random(), input -> fullCheck.accept(parse(input))); + corruptor.forEach(3, test, random(), input -> input.assertError(this::parse)); + } + + public Engine.Result parse(final String expression) { + return engine.parse(expression); + } + + public void testRandom(final int n) { + for (int i = 0; i < n; i++) { + if (i % 100 == 0) { + counter.format("Completed %3d out of %d%n", i, n); + } + final double[] vars = language.randomVars(); + + final Test test = language.randomTest(i); + final double answer = test.evaluate(vars); + + test(test, prepared -> assertValue("random expression", prepared, vars, answer)); + } + } + + public void assertValue(final String context, final Engine.Result prepared, final double[] vars, final double expected) { + counter.test(() -> { + final Engine.Result result = engine.evaluate(prepared, vars); + Asserts.assertEquals("%n\tFor %s%s".formatted(context, result.context()), expected, result.value().doubleValue(), EPS); + }); + } + + public static int mode(final String[] args, final Class type, final String... modes) { + if (args.length == 0) { + System.err.println("ERROR: No arguments found"); + } else if (args.length > 1) { + System.err.println("ERROR: Only one argument expected, " + args.length + " found"); + } else if (!Arrays.asList(modes).contains(args[0])) { + System.err.println("ERROR: First argument should be one of: \"" + String.join("\", \"", modes) + "\", found: \"" + args[0] + "\""); + } else { + return Arrays.asList(modes).indexOf(args[0]); + } + System.err.println("Usage: java -ea " + type.getName() + " {" + String.join("|", modes) + "}"); + System.exit(1); + throw new AssertionError("Return from System.exit"); + } + + public void addStage(final Runnable stage) { + stages.add(stage); + } + + public interface Func extends ToDoubleFunction { + @Override + double applyAsDouble(double... args); + } + + public record Test(Expr expr, String parsed, String unparsed, String toStr, List variables) { + public double evaluate(final double... vars) { + return expr.evaluate(vars); + } + + public Stream points() { + final int n = limit(variables.size()); + return IntStream.range(0, N).mapToObj(i -> IntStream.iterate(i, j -> j / n) + .map(j -> j % n) + .limit(variables.size()) + .mapToDouble(j -> j) + .toArray()); + } + } + + public record BadInput(String prefix, String comment, String suffix) { + public String assertError(final Function> parse) { + try { + final Engine.Result parsed = parse.apply(prefix + suffix); + throw new AssertionError("Parsing error expected for '%s%s%s', got %s" + .formatted(prefix, comment, suffix, parsed.value())); + } catch (final EngineException e) { + return e.getCause().getMessage(); + } + } + } + + public interface Generator { + void generate(String input, Expr expr, ExtendedRandom random, Stream.Builder builder); + + static Generator empty() { + return (i, e, r, b) -> {}; + } + + default Generator combine(final Generator that) { + return (i, e, r, b) -> { + this.generate(i, e, r, b); + that.generate(i, e, r, b); + }; + } + + default void forEach(final int limit, final Test test, final ExtendedRandom random, final Consumer consumer) { + final Stream.Builder builder = Stream.builder(); + generate(test.unparsed(), test.expr(), random, builder); + builder.build() + .sorted(Comparator.comparingInt(Object::hashCode)) + .limit(limit) + .forEach(consumer); + } + } +} diff --git a/common/common/expression/Language.java b/common/common/expression/Language.java new file mode 100644 index 0000000..12f76a2 --- /dev/null +++ b/common/common/expression/Language.java @@ -0,0 +1,64 @@ +package common.expression; + +import common.expression.ExprTester.Test; + +import java.util.Collections; +import java.util.List; + +/** + * Expression language. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Language { + private final Dialect parsed; + private final Dialect unparsed; + private final Dialect toString; + private final BaseVariant variant; + private final List tests; + private final List simplifications; + + public Language( + final Dialect parsed, + final Dialect unparsed, + final Dialect toString, + final BaseVariant variant, + final List simplifications + ) { + this.parsed = parsed; + this.unparsed = unparsed; + this.toString = toString; + this.variant = variant; + + tests = variant.getTests().stream().map(this::test).toList(); + assert simplifications == null || simplifications.isEmpty() || simplifications.size() == tests.size(); + this.simplifications = simplifications != null && simplifications.isEmpty() + ? Collections.nCopies(tests.size(), null) : simplifications; + } + + private Test test(final Expr expr) { + return new Test( + expr, + parsed.render(expr), + unparsed.render(expr), + toString.render(expr), + variant.getVariables() + ); + } + + public Test randomTest(final int size) { + return test(variant.randomTest(size)); + } + + public double[] randomVars() { + return variant.random().getRandom().doubles().limit(variant.getVariables().size()).toArray(); + } + + public List getTests() { + return tests; + } + + public List getSimplifications() { + return simplifications; + } +} diff --git a/common/common/expression/LanguageBuilder.java b/common/common/expression/LanguageBuilder.java new file mode 100644 index 0000000..0756c90 --- /dev/null +++ b/common/common/expression/LanguageBuilder.java @@ -0,0 +1,79 @@ +package common.expression; + +import base.Selector; +import base.TestCounter; +import base.Tester; + +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntPredicate; + +/** + * Expression test builder. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class LanguageBuilder { + public final OperationsBuilder ops; + private List simplifications; + private Function, Expr.Cata> toStr = Function.identity(); + private ExprTester.Generator corruptor = ExprTester.Generator.empty(); + + public LanguageBuilder(final boolean testMulti, final List variables) { + ops = new ArithmeticBuilder(testMulti, variables); + } + + public static Selector.Composite selector( + final Class owner, + final IntPredicate testMulti, + final List variables, + final BiFunction tester, + final String... modes + ) { + return Selector.composite( + owner, + counter -> new LanguageBuilder(testMulti.test(counter.mode()), variables), + (builder, counter) -> tester.apply(builder, counter).test(), + modes + ); + } + + public static Selector.Composite selector( + final Class owner, + final IntPredicate testMulti, + final BiFunction tester, + final String... modes + ) { + return selector(owner, testMulti, List.of("x", "y", "z"), tester, modes); + } + + public Variant variant() { + return ops.variant(); + } + + public Language language(final Dialect parsed, final Dialect unparsed) { + final BaseVariant variant = ops.variant(); + return new Language(parsed.renamed(variant::resolve), unparsed.updated(toStr::apply), unparsed, variant, simplifications); + } + + public void toStr(final Function, Expr.Cata> updater) { + toStr = updater.compose(toStr); + } + + public Function, Expr.Cata> getToStr() { + return toStr; + } + + public void setSimplifications(final List simplifications) { + this.simplifications = simplifications; + } + + public void addCorruptor(final ExprTester.Generator corruptor) { + this.corruptor = this.corruptor.combine(corruptor); + } + + public ExprTester.Generator getCorruptor() { + return corruptor; + } +} diff --git a/common/common/expression/Operation.java b/common/common/expression/Operation.java new file mode 100644 index 0000000..93fd888 --- /dev/null +++ b/common/common/expression/Operation.java @@ -0,0 +1,11 @@ +package common.expression; + +import java.util.function.Consumer; + +/** + * Expression operation. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Operation extends Consumer { +} diff --git a/common/common/expression/Operations.java b/common/common/expression/Operations.java new file mode 100644 index 0000000..2a1723f --- /dev/null +++ b/common/common/expression/Operations.java @@ -0,0 +1,195 @@ +package common.expression; + +import java.util.Arrays; +import java.util.List; +import java.util.OptionalDouble; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Function; +import java.util.function.ToDoubleFunction; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Known expression operations. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("StaticMethodOnlyUsedInOneClass") +public enum Operations { + ; + + public static final Operation ARITH = builder -> { + builder.ops.alias("negate", "Negate"); + builder.ops.alias("+", "Add"); + builder.ops.alias("-", "Subtract"); + builder.ops.alias("*", "Multiply"); + builder.ops.alias("/", "Divide"); + }; + + public static final Operation NARY_ARITH = builder -> { + builder.ops.remove("negate", "+", "-", "*", "/"); + + builder.ops.unary("negate", "Negate", a -> -a); + + builder.ops.any("+", "Add", arith(0, Double::sum, 0)); + builder.ops.any("-", "Subtract", arith(0, (a, b) -> a - b, 1)); + builder.ops.any("*", "Multiply", arith(1, (a, b) -> a * b, 0)); + builder.ops.any("/", "Divide", arith(1, (a, b) -> a / b, 1)); + }; + + + // === Common + + public static Operation constant(final String name, final double value) { + return constant(name, name, value); + } + + public static Operation constant(final String name, final String alias, final double value) { + return builder -> builder.ops.constant(name, alias, value); + } + + public static Operation unary(final String name, final String alias, final DoubleUnaryOperator op) { + return builder -> builder.ops.unary(name, alias, op); + } + + public static Operation binary(final String name, final String alias, final DoubleBinaryOperator op) { + return builder -> builder.ops.binary(name, alias, op); + } + + public static AnyOp arith(final double zero, final DoubleBinaryOperator f, final int minArity) { + final ExprTester.Func func = args -> args.length == 0 ? zero + : args.length == 1 ? f.applyAsDouble(zero, args[0]) + : Arrays.stream(args).reduce(f).orElseThrow(); + return new AnyOp(func, minArity, minArity + 5, 2); + } + + + // === More common + + public record Op(String name, String alias, int minArity, int maxArity, ExprTester.Func f) { + public Operation fix(final int arity) { + assert minArity <= arity && arity <= maxArity; + final String patched = + name.contains("₀") ? name.replace("₀", fix(arity, '₀')) : + name.contains("0") ? name.replace("0", fix(arity, '0')) : + name + fix(arity, name.charAt(0) > 0xff ? '₀' : '0'); + return fixed(patched, alias + arity, arity, f); + } + + private static String fix(final int arity, final char c) { + return "" + (char) (c + arity); + } + + public Operation any(final int fixedArity) { + return checker -> checker.ops.any(name, alias, new AnyOp(f, minArity, maxArity, fixedArity)); + } + } + + public static Op op(final String name, final String alias, final int minArity, final ExprTester.Func f) { + return new Op(name, alias, minArity, minArity + 5, f); + } + + public static Op op1(final String alias, final int minArity, final ExprTester.Func f) { + return new Op(Character.toLowerCase(alias.charAt(0)) + alias.substring(1), alias, minArity, minArity + 5, f); + } + + public static Op opS(final String name, final String alias, final int minArity, final ToDoubleFunction f) { + return op(name, alias, minArity, args -> f.applyAsDouble(Arrays.stream(args))); + } + + public static Op opO(final String name, final String alias, final int minArity, final Function f) { + return opS(name, alias, minArity, f.andThen(OptionalDouble::orElseThrow)::apply); + } + + public static Operation fixed(final String name, final String alias, final int arity, final ExprTester.Func f) { + return builder -> builder.ops.fixed(name, alias, arity, f); + } + + @SuppressWarnings("SameParameterValue") + public static Operation range(final int min, final int max, final Op... ops) { + final List operations = IntStream.rangeClosed(min, max) + .mapToObj(i -> Arrays.stream(ops).map(op -> op.fix(i))) + .flatMap(Function.identity()) + .toList(); + return builder -> operations.forEach(op -> op.accept(builder)); + } + + @SuppressWarnings("SameParameterValue") + public static Operation any(final int fixed, final Op... ops) { + final List operations = Arrays.stream(ops).map(op -> op.any(fixed)).toList(); + return builder -> operations.forEach(op -> op.accept(builder)); + } + + public static Operation infix( + final String name, + final String alias, + final int priority, + final DoubleBinaryOperator op + ) { + return checker -> checker.ops.infix(name, alias, priority, op); + } + + + // === Variables + public static final Operation VARIABLES = builder -> + Stream.of("y", "z").forEach(builder.ops::variable); + + + // === OneTwo + public static final Operation ONE = constant("one", 1); + public static final Operation TWO = constant("two", 2); + public static final Operation THREE = constant("three", 3); + + + // === Clamp, wrap + public static final Operation CLAMP = fixed("clamp", "Clamp", 3, args -> + args[1] <= args[2] ? Math.min(Math.max(args[0], args[1]), args[2]) : Double.NaN); + public static final Operation WRAP = fixed("wrap", "Wrap", 3, args -> + args[1] < args[2] + ? args[0] - Math.floor((args[0] - args[1]) / (args[2] - args[1])) * (args[2] - args[1]) + : Double.NaN); + + // === ArcTan + public static final Operation ATAN = unary("atan", "ArcTan", Math::atan); + public static final Operation ATAN2 = binary("atan2", "ArcTan2", Math::atan2); + + // === ArgMin, ArgMax + + private static Op arg(final String name, final String alias, final Function f) { + return opO( + name, "Arg" + alias, 1, args -> { + final double[] values = args.toArray(); + return f.apply(Arrays.stream(values)).stream() + .flatMap(value -> IntStream.range(0, values.length) + .filter(i -> values[i] == value).asDoubleStream()) + .findFirst(); + } + ); + } + + public static final Op ARG_MIN = arg("argMin", "Min", DoubleStream::min); + public static final Op ARG_MAX = arg("argMax", "Max", DoubleStream::max); + + + // === SoftClamp + public static final Operation SOFT_CLAMP = fixed("softClamp", "SoftClamp", 4, args -> + args[1] <= args[2] && args[3] > 0 + ? args[1] + (args[2] - args[1]) / (1 + Math.exp(args[3] * ((args[2] + args[1]) / 2 - args[0]))) + : Double.NaN); + + + // === SinCos + public static final Operation SIN = unary("sin", "Sin", Math::sin); + public static final Operation COS = unary("cos", "Cos", Math::cos); + + + // === Pow, Log + public static final Operation POW = binary("pow", "Power", Math::pow); + public static final Operation LOG = binary("log", "Log", (a, b) -> Math.log(Math.abs(b)) / Math.log(Math.abs(a))); + + + // === Sum + public static final Op SUM = opS("sum", "Sum", 0, DoubleStream::sum); +} diff --git a/common/common/expression/OperationsBuilder.java b/common/common/expression/OperationsBuilder.java new file mode 100644 index 0000000..c3b203e --- /dev/null +++ b/common/common/expression/OperationsBuilder.java @@ -0,0 +1,31 @@ +package common.expression; + +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleUnaryOperator; + +/** + * Operations builder. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface OperationsBuilder { + void constant(String name, String alias, double value); + + void variable(String name); + + void unary(String name, String alias, DoubleUnaryOperator op); + + void binary(String name, String alias, DoubleBinaryOperator op); + + void infix(String name, String alias, int priority, DoubleBinaryOperator op); + + void fixed(String name, String alias, int arity, ExprTester.Func f); + + void any(String name, String alias, AnyOp op); + + void alias(String name, String alias); + + void remove(String... names); + + BaseVariant variant(); +} diff --git a/common/common/expression/Variant.java b/common/common/expression/Variant.java new file mode 100644 index 0000000..2aa8ba2 --- /dev/null +++ b/common/common/expression/Variant.java @@ -0,0 +1,16 @@ +package common.expression; + +import base.ExtendedRandom; + +/** + * Expression variant meta-info. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Variant { + ExtendedRandom random(); + + boolean hasVarargs(); + + Integer getPriority(String op); +} diff --git a/java/expression/Abs.java b/java/expression/Abs.java new file mode 100644 index 0000000..b1426f9 --- /dev/null +++ b/java/expression/Abs.java @@ -0,0 +1,39 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Abs extends AbstractExpression { + private final AbstractExpression operand; + + public Abs(AbstractExpression operand) { + this.operand = operand; + } + + private static int absInt(int n) { + if (n == Integer.MIN_VALUE) throw new OverflowException("abs"); + return n < 0 ? -n : n; + } + + @Override public int evaluate(int x) { return absInt(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return absInt(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return absInt(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { return operand.evaluateBi(vars).abs(); } + @Override public BigDecimal evaluateBd(List vars) { return operand.evaluateBd(vars).abs(); } + + @Override public String toString() { return "‖" + operand + "‖"; } + @Override public String toMiniString() { return "‖" + operand.toMiniString() + "‖"; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Abs)) return false; + return operand.equals(((Abs) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x41425321; } +} \ No newline at end of file diff --git a/java/expression/AbstractBinaryOperation.java b/java/expression/AbstractBinaryOperation.java new file mode 100644 index 0000000..625d46a --- /dev/null +++ b/java/expression/AbstractBinaryOperation.java @@ -0,0 +1,114 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public abstract class AbstractBinaryOperation extends AbstractExpression { + + protected final AbstractExpression left; + protected final AbstractExpression right; + + protected AbstractBinaryOperation( + AbstractExpression left, + AbstractExpression right + ) { + this.left = left; + this.right = right; + } + + protected abstract String getOperator(); + + protected abstract int getPriority(); + + protected abstract boolean isRightAssoc(); + + protected abstract int applyInt(int a, int b); + + protected abstract BigInteger applyBi(BigInteger a, BigInteger b); + + protected abstract BigDecimal applyBd(BigDecimal a, BigDecimal b); + + @Override + public int evaluate(int x) { + return applyInt(left.evaluate(x), right.evaluate(x)); + } + + @Override + public int evaluate(int x, int y, int z) { + return applyInt(left.evaluate(x, y, z), right.evaluate(x, y, z)); + } + + @Override + public int evaluate(List vars) { + return applyInt(left.evaluate(vars), right.evaluate(vars)); + } + + @Override + public BigInteger evaluateBi(List vars) { + return applyBi(left.evaluateBi(vars), right.evaluateBi(vars)); + } + + @Override + public BigDecimal evaluateBd(List vars) { + return applyBd(left.evaluateBd(vars), right.evaluateBd(vars)); + } + + @Override + public String toString() { + return "(" + left + " " + getOperator() + " " + right + ")"; + } + + @Override + public String toMiniString() { + return miniLeft() + " " + getOperator() + " " + miniRight(); + } + + private String miniLeft() { + if ( + left instanceof AbstractBinaryOperation op && + op.getPriority() < getPriority() + ) { + return "(" + op.toMiniString() + ")"; + } + return left.toMiniString(); + } + + private String miniRight() { + if (right instanceof AbstractBinaryOperation op) { + boolean samePriority = op.getPriority() == getPriority(); + boolean lowerPriority = op.getPriority() < getPriority(); + boolean needParens = + lowerPriority || + (samePriority && isRightAssoc()) || + (samePriority && op.isRightAssoc() && getPriority() > 1) || + (samePriority && + !getOperator().equals(op.getOperator()) && + !op.isRightAssoc() && + !isRightAssoc()); + if (needParens) { + return "(" + op.toMiniString() + ")"; + } + } + return right.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + AbstractBinaryOperation other = (AbstractBinaryOperation) obj; + return left.equals(other.left) && right.equals(other.right); + } + + @Override + public int hashCode() { + return ( + 31 * (31 * left.hashCode() + right.hashCode()) + + getClass().hashCode() + ); + } +} diff --git a/java/expression/AbstractExpression.java b/java/expression/AbstractExpression.java new file mode 100644 index 0000000..d955117 --- /dev/null +++ b/java/expression/AbstractExpression.java @@ -0,0 +1,42 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public abstract class AbstractExpression + implements + Expression, + TripleExpression, + ListExpression, + BigIntegerListExpression, + BigDecimalListExpression +{ + + @Override + public abstract int evaluate(int x); + + @Override + public abstract int evaluate(int x, int y, int z); + + @Override + public abstract int evaluate(List vars); + + @Override + public abstract BigInteger evaluateBi(List vars); + + @Override + public abstract BigDecimal evaluateBd(List vars); + + @Override + public abstract String toString(); + + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract int hashCode(); +} diff --git a/java/expression/Add.java b/java/expression/Add.java new file mode 100644 index 0000000..38bb5e6 --- /dev/null +++ b/java/expression/Add.java @@ -0,0 +1,44 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Add extends AbstractBinaryOperation { + + public Add(AbstractExpression l, AbstractExpression r) { + super(l, r); + } + + @Override + protected String getOperator() { + return "+"; + } + + @Override + protected int getPriority() { + return 1; + } + + @Override + protected boolean isRightAssoc() { + return false; + } + + @Override + protected int applyInt(int a, int b) { + return a + b; + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + return a.add(b); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + return a.add(b); + } +} diff --git a/java/expression/BigDecimalListExpression.java b/java/expression/BigDecimalListExpression.java new file mode 100644 index 0000000..7914c38 --- /dev/null +++ b/java/expression/BigDecimalListExpression.java @@ -0,0 +1,363 @@ +package expression; + +import base.Asserts; +import base.Pair; +import base.TestCounter; +import expression.common.ExpressionKind; +import expression.common.Type; +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.IntStream; + +/** + * One-argument arithmetic expression over {@link BigDecimal}s. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +@SuppressWarnings("ClassReferencesSubclass") +public interface BigDecimalListExpression extends ToMiniString { + BigDecimal evaluateBd(List variables); + + // Tests follow. You may temporarily remove everything til the end. + + Add EXAMPLE = new Add( + new Subtract(new Variable(0), new Const(BigDecimal.ONE)), + new Multiply(new Variable(1), new Const(BigDecimal.TEN)) + ); + + Type TYPE = new Type<>( + v -> new BigDecimal(v + ".000"), + random -> BigDecimal.valueOf(random.getRandom().nextGaussian()), + BigDecimal.class + ); + ExpressionKind KIND = + new ExpressionKind<>( + TYPE, + BigDecimalListExpression.class, + (r, c) -> + IntStream.range(0, c) + .mapToObj(name -> + Pair.of( + "$" + name, + new Variable(name) + ) + ) + .toList(), + (expr, variables, values) -> expr.evaluateBd(values) + ); + + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + static ExpressionTester tester(final TestCounter counter) { + Asserts.assertEquals( + "Example toString()", + "(($0 - 1) + ($1 * 10))", + EXAMPLE.toString() + ); + Asserts.assertEquals( + EXAMPLE + " at (2, 3)", + BigDecimal.valueOf(31), + EXAMPLE.evaluateBd( + List.of(BigDecimal.valueOf(2), BigDecimal.valueOf(3)) + ) + ); + + final Variable vx = new Variable(0); + final Variable vy = new Variable(1); + + return new ExpressionTester<>( + counter, + KIND, + c -> v -> c, + (op, a, b) -> v -> op.apply(a.evaluateBd(v), b.evaluateBd(v)), + BigDecimal::add, + BigDecimal::subtract, + BigDecimal::multiply, + BigDecimal::divide + ) + .basic("10", "10", v -> v(10), c(10)) + .basic("$x", "$x", BigDecimalListExpression::x, vx) + .basic("$y", "$y", BigDecimalListExpression::y, vy) + .basic("($x + $y)", "$x + $y", v -> x(v).add(y(v)), new Add(vx, vy)) + .basic("($x + 2)", "$x + 2", v -> x(v).add(v(2)), new Add(vx, c(2))) + .basic( + "(2 - $x)", + "2 - $x", + v -> v(2).subtract(x(v)), + new Subtract(c(2), vx) + ) + .basic( + "(3 * $x)", + "3 * $x", + v -> v(3).multiply(x(v)), + new Multiply(c(3), vx) + ) + .basic("($x + $x)", "$x + $x", v -> x(v).add(x(v)), new Add(vx, vx)) + .basic( + "($x / -2)", + "$x / -2", + v -> x(v).divide(v(-2)), + new Divide(vx, c(-2)) + ) + .basic("(2 + $x)", "2 + $x", v -> v(2).add(x(v)), new Add(c(2), vx)) + .basic( + "((1 + 2) + 3)", + "1 + 2 + 3", + v -> v(6), + new Add(new Add(c(1), c(2)), c(3)) + ) + .basic( + "(1 + (2 * 3))", + "1 + 2 * 3", + v -> v(7), + new Add(c(1), new Multiply(c(2), c(3))) + ) + .basic( + "(1 - (2 * 3))", + "1 - 2 * 3", + v -> v(-5), + new Subtract(c(1), new Multiply(c(2), c(3))) + ) + .basic( + "(1 + (2 + 3))", + "1 + 2 + 3", + v -> v(6), + new Add(c(1), new Add(c(2), c(3))) + ) + .basic( + "((1 - 2) - 3)", + "1 - 2 - 3", + v -> v(-4), + new Subtract(new Subtract(c(1), c(2)), c(3)) + ) + .basic( + "(1 - (2 - 3))", + "1 - (2 - 3)", + v -> v(2), + new Subtract(c(1), new Subtract(c(2), c(3))) + ) + .basic( + "((1 * 2) * 3)", + "1 * 2 * 3", + v -> v(6), + new Multiply(new Multiply(c(1), c(2)), c(3)) + ) + .basic( + "(1 * (2 * 3))", + "1 * 2 * 3", + v -> v(6), + new Multiply(c(1), new Multiply(c(2), c(3))) + ) + .basic( + "((10 / 2) / 3)", + "10 / 2 / 3", + v -> v(10).divide(v(2)).divide(v(3)), + new Divide(new Divide(c(10), c(2)), c(3)) + ) + .basic( + "(10 / (3 / 2))", + "10 / (3 / 2)", + v -> v(10).divide(v(3).divide(v(2))), + new Divide(c(10), new Divide(c(3), c(2))) + ) + .basic( + "(($x * $x) + (($x - 1) / 10))", + "$x * $x + ($x - 1) / 10", + v -> x(v).multiply(x(v)).add(x(v).subtract(v(1)).divide(v(10))), + new Add( + new Multiply(vx, vx), + new Divide(new Subtract(vx, c(1)), c(10)) + ) + ) + .basic( + "($x * -1000000000)", + "$x * -1000000000", + v -> x(v).multiply(v(-1_000_000_000)), + new Multiply(vx, c(-1_000_000_000)) + ) + .basic( + "($x * -1000000000000000)", + "$x * -1000000000000000", + v -> x(v).multiply(v(-1_000_000_000_000_000L)), + new Multiply(vx, c(-1_000_000_000_000_000L)) + ) + .basic( + "(10 / $x)", + "10 / $x", + v -> v(10).divide(x(v)), + new Divide(c(10), vx) + ) + .basic( + "($x / $x)", + "$x / $x", + v -> x(v).divide(x(v)), + new Divide(vx, vx) + ) + .advanced("(2 + 1)", "2 + 1", v -> v(2 + 1), new Add(c(2), c(1))) + .advanced( + "($x - 1)", + "$x - 1", + v -> x(v).subtract(v(1)), + new Subtract(vx, c(1)) + ) + .advanced( + "(1 * 2)", + "1 * 2", + v -> v(1 * 2), + new Multiply(c(1), c(2)) + ) + .advanced( + "($x / 1)", + "$x / 1", + v -> x(v).divide(v(1)), + new Divide(vx, c(1)) + ) + .advanced( + "(1 + (2 + 1))", + "1 + 2 + 1", + v -> v(1 + 2 + 1), + new Add(c(1), new Add(c(2), c(1))) + ) + .advanced( + "($x - ($x - 1))", + "$x - ($x - 1)", + v -> x(v).subtract(x(v).subtract(v(1))), + new Subtract(vx, new Subtract(vx, c(1))) + ) + .advanced( + "(2 * ($x / 1))", + "2 * ($x / 1)", + v -> v(2).multiply(x(v).divide(v(1))), + new Multiply(c(2), new Divide(vx, c(1))) + ) + .advanced( + "(2 / ($x - 1))", + "2 / ($x - 1)", + v -> v(2).divide(x(v).subtract(v(1))), + new Divide(c(2), new Subtract(vx, c(1))) + ) + .advanced( + "((1 * 2) + $x)", + "1 * 2 + $x", + v -> v(1 * 2).add(x(v)), + new Add(new Multiply(c(1), c(2)), vx) + ) + .advanced( + "(($x - 1) - 2)", + "$x - 1 - 2", + v -> x(v).subtract(v(3)), + new Subtract(new Subtract(vx, c(1)), c(2)) + ) + .advanced( + "(($x / 1) * 2)", + "$x / 1 * 2", + v -> x(v).multiply(v(2)), + new Multiply(new Divide(vx, c(1)), c(2)) + ) + .advanced( + "((2 + 1) / 1)", + "(2 + 1) / 1", + v -> v(3), + new Divide(new Add(c(2), c(1)), c(1)) + ) + .advanced( + "(1 + (1 + (2 + 1)))", + "1 + 1 + 2 + 1", + v -> v(1 + 1 + 2 + 1), + new Add(c(1), new Add(c(1), new Add(c(2), c(1)))) + ) + .advanced( + "($x - ((1 * 2) + $x))", + "$x - (1 * 2 + $x)", + v -> x(v).subtract(v(1 * 2).add(x(v))), + new Subtract(vx, new Add(new Multiply(c(1), c(2)), vx)) + ) + .advanced( + "($x * (2 / ($x - 1)))", + "$x * (2 / ($x - 1))", + v -> x(v).multiply(v(2).divide(x(v).subtract(v(1)))), + new Multiply(vx, new Divide(c(2), new Subtract(vx, c(1)))) + ) + .advanced( + "($x / (1 + (2 + 1)))", + "$x / (1 + 2 + 1)", + v -> x(v).divide(v(1 + 2 + 1)), + new Divide(vx, new Add(c(1), new Add(c(2), c(1)))) + ) + .advanced( + "((1 * 2) + (2 + 1))", + "1 * 2 + 2 + 1", + v -> v(1 * 2 + 2 + 1), + new Add(new Multiply(c(1), c(2)), new Add(c(2), c(1))) + ) + .advanced( + "((2 + 1) - (2 + 1))", + "2 + 1 - (2 + 1)", + v -> v(2 + 1 - (2 + 1)), + new Subtract(new Add(c(2), c(1)), new Add(c(2), c(1))) + ) + .advanced( + "(($x - 1) * ($x / 1))", + "($x - 1) * ($x / 1)", + v -> x(v).subtract(v(1)).multiply(x(v).divide(v(1))), + new Multiply(new Subtract(vx, c(1)), new Divide(vx, c(1))) + ) + .advanced( + "(($x - 1) / (1 * 2))", + "($x - 1) / (1 * 2)", + v -> x(v).subtract(v(1)).divide(v(2)), + new Divide(new Subtract(vx, c(1)), new Multiply(c(1), c(2))) + ) + .advanced( + "((($x - 1) - 2) + $x)", + "$x - 1 - 2 + $x", + v -> x(v).subtract(v(3)).add(x(v)), + new Add(new Subtract(new Subtract(vx, c(1)), c(2)), vx) + ) + .advanced( + "(((1 * 2) + $x) - 1)", + "1 * 2 + $x - 1", + v -> v(1).add(x(v)), + new Subtract(new Add(new Multiply(c(1), c(2)), vx), c(1)) + ) + .advanced( + "(((2 + 1) / 1) * $x)", + "(2 + 1) / 1 * $x", + v -> v(3).multiply(x(v)), + new Multiply(new Divide(new Add(c(2), c(1)), c(1)), vx) + ) + .advanced( + "((2 / ($x - 1)) / 2)", + "2 / ($x - 1) / 2", + v -> v(2).divide(x(v).subtract(v(1))).divide(v(2)), + new Divide(new Divide(c(2), new Subtract(vx, c(1))), c(2)) + ); + } + + private static BigDecimal x(final List vars) { + return vars.get(0); + } + + private static BigDecimal y(final List vars) { + return vars.get(1); + } + + private static Const c(final BigDecimal v) { + return TYPE.constant(v); + } + + private static Const c(final long v) { + return TYPE.constant(v(v)); + } + + private static BigDecimal v(final long v) { + return BigDecimal.valueOf(v); + } + + static void main(final String... args) { + TripleExpression.SELECTOR.variant( + "BigDecimalList", + ExpressionTest.v(BigDecimalListExpression::tester) + ).main(args); + } +} diff --git a/java/expression/BigIntegerListExpression.java b/java/expression/BigIntegerListExpression.java new file mode 100644 index 0000000..92128c2 --- /dev/null +++ b/java/expression/BigIntegerListExpression.java @@ -0,0 +1,357 @@ +package expression; + +import base.Asserts; +import base.Pair; +import base.TestCounter; +import expression.common.ExpressionKind; +import expression.common.Type; +import java.math.BigInteger; +import java.util.List; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +@SuppressWarnings("ClassReferencesSubclass") +public interface BigIntegerListExpression extends ToMiniString { + BigInteger evaluateBi(List variables); + + // Tests follow. You may temporarily remove everything til the end. + + Add EXAMPLE = new Add( + new Subtract(new Variable(0), new Const(BigInteger.ONE)), + new Multiply(new Variable(1), new Const(BigInteger.TEN)) + ); + + Type TYPE = new Type<>( + BigInteger::valueOf, + random -> v(random.getRandom().nextLong()), + BigInteger.class + ); + ExpressionKind KIND = + new ExpressionKind<>( + TYPE, + BigIntegerListExpression.class, + (r, c) -> + IntStream.range(0, c) + .mapToObj(name -> + Pair.of( + "$" + name, + new Variable(name) + ) + ) + .toList(), + (expr, variables, values) -> expr.evaluateBi(values) + ); + + @SuppressWarnings("PointlessArithmeticExpression") + static ExpressionTester tester(final TestCounter counter) { + Asserts.assertEquals( + "Example toString()", + "(($0 - 1) + ($1 * 10))", + EXAMPLE.toString() + ); + Asserts.assertEquals( + EXAMPLE + " at (2, 3)", + BigInteger.valueOf(31), + EXAMPLE.evaluateBi( + List.of(BigInteger.valueOf(2), BigInteger.valueOf(3)) + ) + ); + + final Variable vx = new Variable(0); + final Variable vy = new Variable(1); + + return new ExpressionTester<>( + counter, + KIND, + c -> v -> c, + (op, a, b) -> v -> op.apply(a.evaluateBi(v), b.evaluateBi(v)), + BigInteger::add, + BigInteger::subtract, + BigInteger::multiply, + BigInteger::divide + ) + .basic("10", "10", v -> v(10), c(10)) + .basic("$x", "$x", BigIntegerListExpression::x, vx) + .basic("$y", "$y", BigIntegerListExpression::y, vy) + .basic("($x + $y)", "$x + $y", v -> x(v).add(y(v)), new Add(vx, vy)) + .basic("($x + 2)", "$x + 2", v -> x(v).add(v(2)), new Add(vx, c(2))) + .basic( + "(2 - $x)", + "2 - $x", + v -> v(2).subtract(x(v)), + new Subtract(c(2), vx) + ) + .basic( + "(3 * $x)", + "3 * $x", + v -> v(3).multiply(x(v)), + new Multiply(c(3), vx) + ) + .basic("($x + $x)", "$x + $x", v -> x(v).add(x(v)), new Add(vx, vx)) + .basic( + "($x / -2)", + "$x / -2", + v -> x(v).divide(v(-2)), + new Divide(vx, c(-2)) + ) + .basic("(2 + $x)", "2 + $x", v -> v(2).add(x(v)), new Add(c(2), vx)) + .basic( + "((1 + 2) + 3)", + "1 + 2 + 3", + v -> v(6), + new Add(new Add(c(1), c(2)), c(3)) + ) + .basic( + "(1 + (2 * 3))", + "1 + 2 * 3", + v -> v(7), + new Add(c(1), new Multiply(c(2), c(3))) + ) + .basic( + "(1 - (2 * 3))", + "1 - 2 * 3", + v -> v(-5), + new Subtract(c(1), new Multiply(c(2), c(3))) + ) + .basic( + "(1 + (2 + 3))", + "1 + 2 + 3", + v -> v(6), + new Add(c(1), new Add(c(2), c(3))) + ) + .basic( + "((1 - 2) - 3)", + "1 - 2 - 3", + v -> v(-4), + new Subtract(new Subtract(c(1), c(2)), c(3)) + ) + .basic( + "(1 - (2 - 3))", + "1 - (2 - 3)", + v -> v(2), + new Subtract(c(1), new Subtract(c(2), c(3))) + ) + .basic( + "((1 * 2) * 3)", + "1 * 2 * 3", + v -> v(6), + new Multiply(new Multiply(c(1), c(2)), c(3)) + ) + .basic( + "(1 * (2 * 3))", + "1 * 2 * 3", + v -> v(6), + new Multiply(c(1), new Multiply(c(2), c(3))) + ) + .basic( + "((10 / 2) / 3)", + "10 / 2 / 3", + v -> v(10 / 2 / 3), + new Divide(new Divide(c(10), c(2)), c(3)) + ) + .basic( + "(10 / (3 / 2))", + "10 / (3 / 2)", + v -> v(10 / (3 / 2)), + new Divide(c(10), new Divide(c(3), c(2))) + ) + .basic( + "(($x * $x) + (($x - 1) / 10))", + "$x * $x + ($x - 1) / 10", + v -> x(v).multiply(x(v)).add(x(v).subtract(v(1)).divide(v(10))), + new Add( + new Multiply(vx, vx), + new Divide(new Subtract(vx, c(1)), c(10)) + ) + ) + .basic( + "($x * -1000000000)", + "$x * -1000000000", + v -> x(v).multiply(v(-1_000_000_000)), + new Multiply(vx, c(-1_000_000_000)) + ) + .basic( + "($x * -1000000000000000)", + "$x * -1000000000000000", + v -> x(v).multiply(v(-1_000_000_000_000_000L)), + new Multiply(vx, c(-1_000_000_000_000_000L)) + ) + .basic( + "(10 / $x)", + "10 / $x", + v -> v(10).divide(x(v)), + new Divide(c(10), vx) + ) + .basic( + "($x / $x)", + "$x / $x", + v -> x(v).divide(x(v)), + new Divide(vx, vx) + ) + .advanced("(2 + 1)", "2 + 1", v -> v(2 + 1), new Add(c(2), c(1))) + .advanced( + "($x - 1)", + "$x - 1", + v -> x(v).subtract(v(1)), + new Subtract(vx, c(1)) + ) + .advanced( + "(1 * 2)", + "1 * 2", + v -> v(1 * 2), + new Multiply(c(1), c(2)) + ) + .advanced( + "($x / 1)", + "$x / 1", + v -> x(v).divide(v(1)), + new Divide(vx, c(1)) + ) + .advanced( + "(1 + (2 + 1))", + "1 + 2 + 1", + v -> v(1 + 2 + 1), + new Add(c(1), new Add(c(2), c(1))) + ) + .advanced( + "($x - ($x - 1))", + "$x - ($x - 1)", + v -> x(v).subtract(x(v).subtract(v(1))), + new Subtract(vx, new Subtract(vx, c(1))) + ) + .advanced( + "(2 * ($x / 1))", + "2 * ($x / 1)", + v -> v(2).multiply(x(v).divide(v(1))), + new Multiply(c(2), new Divide(vx, c(1))) + ) + .advanced( + "(2 / ($x - 1))", + "2 / ($x - 1)", + v -> v(2).divide(x(v).subtract(v(1))), + new Divide(c(2), new Subtract(vx, c(1))) + ) + .advanced( + "((1 * 2) + $x)", + "1 * 2 + $x", + v -> v(1 * 2).add(x(v)), + new Add(new Multiply(c(1), c(2)), vx) + ) + .advanced( + "(($x - 1) - 2)", + "$x - 1 - 2", + v -> x(v).subtract(v(3)), + new Subtract(new Subtract(vx, c(1)), c(2)) + ) + .advanced( + "(($x / 1) * 2)", + "$x / 1 * 2", + v -> x(v).multiply(v(2)), + new Multiply(new Divide(vx, c(1)), c(2)) + ) + .advanced( + "((2 + 1) / 1)", + "(2 + 1) / 1", + v -> v(3), + new Divide(new Add(c(2), c(1)), c(1)) + ) + .advanced( + "(1 + (1 + (2 + 1)))", + "1 + 1 + 2 + 1", + v -> v(1 + 1 + 2 + 1), + new Add(c(1), new Add(c(1), new Add(c(2), c(1)))) + ) + .advanced( + "($x - ((1 * 2) + $x))", + "$x - (1 * 2 + $x)", + v -> x(v).subtract(v(1 * 2).add(x(v))), + new Subtract(vx, new Add(new Multiply(c(1), c(2)), vx)) + ) + .advanced( + "($x * (2 / ($x - 1)))", + "$x * (2 / ($x - 1))", + v -> x(v).multiply(v(2).divide(x(v).subtract(v(1)))), + new Multiply(vx, new Divide(c(2), new Subtract(vx, c(1)))) + ) + .advanced( + "($x / (1 + (2 + 1)))", + "$x / (1 + 2 + 1)", + v -> x(v).divide(v(1 + 2 + 1)), + new Divide(vx, new Add(c(1), new Add(c(2), c(1)))) + ) + .advanced( + "((1 * 2) + (2 + 1))", + "1 * 2 + 2 + 1", + v -> v(1 * 2 + 2 + 1), + new Add(new Multiply(c(1), c(2)), new Add(c(2), c(1))) + ) + .advanced( + "((2 + 1) - (2 + 1))", + "2 + 1 - (2 + 1)", + v -> v(2 + 1 - (2 + 1)), + new Subtract(new Add(c(2), c(1)), new Add(c(2), c(1))) + ) + .advanced( + "(($x - 1) * ($x / 1))", + "($x - 1) * ($x / 1)", + v -> x(v).subtract(v(1)).multiply(x(v).divide(v(1))), + new Multiply(new Subtract(vx, c(1)), new Divide(vx, c(1))) + ) + .advanced( + "(($x - 1) / (1 * 2))", + "($x - 1) / (1 * 2)", + v -> x(v).subtract(v(1)).divide(v(2)), + new Divide(new Subtract(vx, c(1)), new Multiply(c(1), c(2))) + ) + .advanced( + "((($x - 1) - 2) + $x)", + "$x - 1 - 2 + $x", + v -> x(v).subtract(v(3)).add(x(v)), + new Add(new Subtract(new Subtract(vx, c(1)), c(2)), vx) + ) + .advanced( + "(((1 * 2) + $x) - 1)", + "1 * 2 + $x - 1", + v -> v(1).add(x(v)), + new Subtract(new Add(new Multiply(c(1), c(2)), vx), c(1)) + ) + .advanced( + "(((2 + 1) / 1) * $x)", + "(2 + 1) / 1 * $x", + v -> v(3).multiply(x(v)), + new Multiply(new Divide(new Add(c(2), c(1)), c(1)), vx) + ) + .advanced( + "((2 / ($x - 1)) / 2)", + "2 / ($x - 1) / 2", + v -> v(2).divide(x(v).subtract(v(1))).divide(v(2)), + new Divide(new Divide(c(2), new Subtract(vx, c(1))), c(2)) + ); + } + + private static BigInteger x(final List vars) { + return vars.get(0); + } + + private static BigInteger y(final List vars) { + return vars.get(1); + } + + private static Const c(final long v) { + return TYPE.constant(v(v)); + } + + private static BigInteger v(final long v) { + return BigInteger.valueOf(v); + } + + static void main(final String... args) { + TripleExpression.SELECTOR.variant( + "BigIntegerList", + ExpressionTest.v(BigIntegerListExpression::tester) + ).main(args); + } +} diff --git a/java/expression/Cbrt.java b/java/expression/Cbrt.java new file mode 100644 index 0000000..9d8c5dd --- /dev/null +++ b/java/expression/Cbrt.java @@ -0,0 +1,62 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Cbrt extends AbstractExpression { + private final AbstractExpression operand; + + public Cbrt(AbstractExpression operand) { + this.operand = operand; + } + + private static int cbrtInt(int n) { + if (n == 0) return 0; + boolean negative = n < 0; + int abs = (n == Integer.MIN_VALUE) ? Integer.MAX_VALUE : (negative ? -n : n); + + int lo = 0, hi = 1290; + while (lo < hi) { + int mid = (lo + hi + 1) / 2; + if (mid * mid * mid <= abs) { + lo = mid; + } else { + hi = mid - 1; + } + } + return negative ? -lo : lo; + } + + @Override public int evaluate(int x) { return cbrtInt(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return cbrtInt(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return cbrtInt(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { + throw new UnsupportedOperationException("∛ not supported for BigInteger"); + } + @Override public BigDecimal evaluateBd(List vars) { + throw new UnsupportedOperationException("∛ not supported for BigDecimal"); + } + + @Override public String toString() { return "∛(" + operand + ")"; } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "∛(" + operand.toMiniString() + ")"; + } + return "∛" + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Cbrt)) return false; + return operand.equals(((Cbrt) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x43425254; } +} \ No newline at end of file diff --git a/java/expression/Ceiling.java b/java/expression/Ceiling.java new file mode 100644 index 0000000..c8a747a --- /dev/null +++ b/java/expression/Ceiling.java @@ -0,0 +1,75 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Ceiling extends AbstractExpression { + + private final AbstractExpression operand; + + public Ceiling(AbstractExpression operand) { + this.operand = operand; + } + + private static int ceilInt(int n) { + return Math.ceilDiv(n, 1000) * 1000; + } + + @Override + public int evaluate(int x) { + return ceilInt(operand.evaluate(x)); + } + + @Override + public int evaluate(int x, int y, int z) { + return ceilInt(operand.evaluate(x, y, z)); + } + + @Override + public int evaluate(List vars) { + return ceilInt(operand.evaluate(vars)); + } + + @Override + public BigInteger evaluateBi(List vars) { + throw new UnsupportedOperationException( + "ceiling not supported for BigInteger" + ); + } + + @Override + public BigDecimal evaluateBd(List vars) { + throw new UnsupportedOperationException( + "ceiling not supported for BigDecimal" + ); + } + + @Override + public String toString() { + return "ceiling(" + operand + ")"; + } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "ceiling(" + operand.toMiniString() + ")"; + } + return "ceiling " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Ceiling)) return false; + return operand.equals(((Ceiling) obj).operand); + } + + @Override + public int hashCode() { + return operand.hashCode() ^ 0x43454C47; + } +} diff --git a/java/expression/Clear.java b/java/expression/Clear.java new file mode 100644 index 0000000..4a9aba8 --- /dev/null +++ b/java/expression/Clear.java @@ -0,0 +1,46 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Clear extends AbstractBinaryOperation { + + public Clear(AbstractExpression l, AbstractExpression r) { + super(l, r); + } + + @Override + protected String getOperator() { + return "clear"; + } + + @Override + protected int getPriority() { + return 0; + } + + @Override + protected boolean isRightAssoc() { + return true; + } + + @Override + protected int applyInt(int a, int b) { + return a & ~(1 << b); + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + return a.clearBit(b.intValueExact()); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + throw new UnsupportedOperationException( + "clear not supported for BigDecimal" + ); + } +} diff --git a/java/expression/Const.java b/java/expression/Const.java new file mode 100644 index 0000000..07e93ed --- /dev/null +++ b/java/expression/Const.java @@ -0,0 +1,84 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Const extends AbstractExpression { + + private final int intValue; + private final BigInteger biValue; + private final BigDecimal bdValue; + private final String repr; + + public Const(int value) { + this.intValue = value; + this.biValue = BigInteger.valueOf(value); + this.bdValue = BigDecimal.valueOf(value); + this.repr = Integer.toString(value); + } + + public Const(BigInteger value) { + this.intValue = value.intValueExact(); + this.biValue = value; + this.bdValue = new BigDecimal(value); + this.repr = value.toString(); + } + + public Const(BigDecimal value) { + this.bdValue = value; + this.biValue = value.toBigIntegerExact(); + this.intValue = biValue.intValueExact(); + this.repr = value.toPlainString(); + } + + @Override + public int evaluate(int x) { + return intValue; + } + + @Override + public int evaluate(int x, int y, int z) { + return intValue; + } + + @Override + public int evaluate(List vars) { + return intValue; + } + + @Override + public BigInteger evaluateBi(List vars) { + return biValue; + } + + @Override + public BigDecimal evaluateBd(List vars) { + return bdValue; + } + + @Override + public String toString() { + return repr; + } + + @Override + public String toMiniString() { + return repr; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Const)) return false; + return repr.equals(((Const) obj).repr); + } + + @Override + public int hashCode() { + return repr.hashCode(); + } +} diff --git a/java/expression/Cube.java b/java/expression/Cube.java new file mode 100644 index 0000000..4ac848d --- /dev/null +++ b/java/expression/Cube.java @@ -0,0 +1,55 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Cube extends AbstractExpression { + private final AbstractExpression operand; + + public Cube(AbstractExpression operand) { + this.operand = operand; + } + + private static int checkedMul(int a, int b) { + if (a == 0 || b == 0) return 0; + if (a == Integer.MIN_VALUE || b == Integer.MIN_VALUE) throw new OverflowException("cube"); + int r = a * b; + if (r / a != b) throw new OverflowException("cube"); + return r; + } + + private static int cubeInt(int n) { + return checkedMul(checkedMul(n, n), n); + } + + @Override public int evaluate(int x) { return cubeInt(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return cubeInt(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return cubeInt(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { return operand.evaluateBi(vars).pow(3); } + @Override public BigDecimal evaluateBd(List vars) { return operand.evaluateBd(vars).pow(3); } + + @Override public String toString() { return "(" + operand + ")³"; } + + @Override + public String toMiniString() { + String s = operand.toMiniString(); + boolean needParens = operand instanceof AbstractBinaryOperation + || operand instanceof Cbrt + || operand instanceof Sqrt + || (s.startsWith("-") && (s.length() < 2 || !Character.isDigit(s.charAt(1)))); + return needParens ? "(" + s + ")³" : s + "³"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Cube)) return false; + return operand.equals(((Cube) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x43554245; } +} \ No newline at end of file diff --git a/java/expression/Digits.java b/java/expression/Digits.java new file mode 100644 index 0000000..5022c06 --- /dev/null +++ b/java/expression/Digits.java @@ -0,0 +1,83 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Digits extends AbstractExpression { + + private final AbstractExpression operand; + + public Digits(AbstractExpression operand) { + this.operand = operand; + } + + private static int digitsSum(int n) { + if (n == 0) return 0; + boolean negative = n < 0; + long abs = Math.abs((long) n); + int sum = 0; + while (abs > 0) { + sum += (int) (abs % 10); + abs /= 10; + } + return negative ? -sum : sum; + } + + @Override + public int evaluate(int x) { + return digitsSum(operand.evaluate(x)); + } + + @Override + public int evaluate(int x, int y, int z) { + return digitsSum(operand.evaluate(x, y, z)); + } + + @Override + public int evaluate(List vars) { + return digitsSum(operand.evaluate(vars)); + } + + @Override + public BigInteger evaluateBi(List vars) { + throw new UnsupportedOperationException( + "digits not supported for BigInteger" + ); + } + + @Override + public BigDecimal evaluateBd(List vars) { + throw new UnsupportedOperationException( + "digits not supported for BigDecimal" + ); + } + + @Override + public String toString() { + return "digits(" + operand + ")"; + } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "digits(" + operand.toMiniString() + ")"; + } + return "digits " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Digits)) return false; + return operand.equals(((Digits) obj).operand); + } + + @Override + public int hashCode() { + return operand.hashCode() ^ 0x44494754; + } +} diff --git a/java/expression/Divide.java b/java/expression/Divide.java new file mode 100644 index 0000000..47e9e03 --- /dev/null +++ b/java/expression/Divide.java @@ -0,0 +1,45 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Divide extends AbstractBinaryOperation { + + public Divide(AbstractExpression l, AbstractExpression r) { + super(l, r); + } + + @Override + protected String getOperator() { + return "/"; + } + + @Override + protected int getPriority() { + return 2; + } + + @Override + protected boolean isRightAssoc() { + return true; + } + + @Override + protected int applyInt(int a, int b) { + return a / b; + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + return a.divide(b); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + return a.divide(b, MathContext.DECIMAL128); + } +} diff --git a/java/expression/DivisionByZeroException.java b/java/expression/DivisionByZeroException.java new file mode 100644 index 0000000..f908ef3 --- /dev/null +++ b/java/expression/DivisionByZeroException.java @@ -0,0 +1,10 @@ +package expression; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class DivisionByZeroException extends ArithmeticException { + public DivisionByZeroException() { + super("division by zero"); + } +} \ No newline at end of file diff --git a/java/expression/Expression.java b/java/expression/Expression.java new file mode 100644 index 0000000..94bb4ce --- /dev/null +++ b/java/expression/Expression.java @@ -0,0 +1,285 @@ +package expression; + +import base.Asserts; +import base.ExtendedRandom; +import base.Pair; +import base.TestCounter; +import expression.common.ExpressionKind; +import expression.common.Type; +import java.util.List; + +/** + * One-argument arithmetic expression over integers. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +@SuppressWarnings("ClassReferencesSubclass") +public interface Expression extends ToMiniString { + int evaluate(int x); + + // Tests follow. You may temporarily remove everything til the end. + + Subtract EXAMPLE = new Subtract( + new Multiply(new Const(2), new Variable("x")), + new Const(3) + ); + + Type TYPE = new Type<>(a -> a, ExtendedRandom::nextInt, int.class); + ExpressionKind KIND = new ExpressionKind<>( + TYPE, + Expression.class, + List.of(Pair.of("x", new Variable("x"))), + (expr, variables, values) -> expr.evaluate(values.get(0)) + ); + + private static Const c(final int c) { + return new Const(c); + } + + @SuppressWarnings({ "PointlessArithmeticExpression", "Convert2MethodRef" }) + static ExpressionTester tester(final TestCounter counter) { + Asserts.assertEquals( + "Example toString()", + "((2 * x) - 3)", + EXAMPLE.toString() + ); + Asserts.assertEquals("Example at 5", 7, EXAMPLE.evaluate(5)); + Asserts.assertTrue( + "Example equals 1", + new Multiply(new Const(2), new Variable("x")).equals( + new Multiply(new Const(2), new Variable("x")) + ) + ); + Asserts.assertTrue( + "Example equals 2", + !new Multiply(new Const(2), new Variable("x")).equals( + new Multiply(new Variable("x"), new Const(2)) + ) + ); + + final Variable vx = new Variable("x"); + final Const c1 = c(1); + final Const c2 = c(2); + + return new ExpressionTester<>( + counter, + KIND, + c -> x -> c, + (op, a, b) -> x -> op.apply(a.evaluate(x), b.evaluate(x)), + (a, b) -> a + b, + (a, b) -> a - b, + (a, b) -> a * b, + (a, b) -> a / b + ) + .basic("10", "10", x -> 10, c(10)) + .basic("x", "x", x -> x, vx) + .basic("(x + 2)", "x + 2", x -> x + 2, new Add(vx, c(2))) + .basic("(2 - x)", "2 - x", x -> 2 - x, new Subtract(c(2), vx)) + .basic("(3 * x)", "3 * x", x -> 3 * x, new Multiply(c(3), vx)) + .basic("(x + x)", "x + x", x -> x + x, new Add(vx, vx)) + .basic("(x / -2)", "x / -2", x -> -x / 2, new Divide(vx, c(-2))) + .basic("(2 + x)", "2 + x", x -> 2 + x, new Add(c(2), vx)) + .basic( + "((1 + 2) + 3)", + "1 + 2 + 3", + x -> 6, + new Add(new Add(c(1), c(2)), c(3)) + ) + .basic( + "(1 + (2 + 3))", + "1 + 2 + 3", + x -> 6, + new Add(c(1), new Add(c(2), c(3))) + ) + .basic( + "((1 - 2) - 3)", + "1 - 2 - 3", + x -> -4, + new Subtract(new Subtract(c(1), c(2)), c(3)) + ) + .basic( + "(1 - (2 - 3))", + "1 - (2 - 3)", + x -> 2, + new Subtract(c(1), new Subtract(c(2), c(3))) + ) + .basic( + "((1 * 2) * 3)", + "1 * 2 * 3", + x -> 6, + new Multiply(new Multiply(c(1), c(2)), c(3)) + ) + .basic( + "(1 * (2 * 3))", + "1 * 2 * 3", + x -> 6, + new Multiply(c(1), new Multiply(c(2), c(3))) + ) + .basic( + "((10 / 2) / 3)", + "10 / 2 / 3", + x -> 10 / 2 / 3, + new Divide(new Divide(c(10), c(2)), c(3)) + ) + .basic( + "(10 / (3 / 2))", + "10 / (3 / 2)", + x -> 10 / (3 / 2), + new Divide(c(10), new Divide(c(3), c(2))) + ) + .basic( + "(10 * (3 / 2))", + "10 * (3 / 2)", + x -> 10 * (3 / 2), + new Multiply(c(10), new Divide(c(3), c(2))) + ) + .basic( + "(10 + (3 - 2))", + "10 + 3 - 2", + x -> 10 + (3 - 2), + new Add(c(10), new Subtract(c(3), c(2))) + ) + .basic( + "((x * x) + ((x - 1) / 10))", + "x * x + (x - 1) / 10", + x -> x * x + (x - 1) / 10, + new Add( + new Multiply(vx, vx), + new Divide(new Subtract(vx, c(1)), c(10)) + ) + ) + .basic( + "(x * -1000000000)", + "x * -1000000000", + x -> x * -1_000_000_000, + new Multiply(vx, c(-1_000_000_000)) + ) + .basic("(10 / x)", "10 / x", x -> 10 / x, new Divide(c(10), vx)) + .basic("(x / x)", "x / x", x -> x / x, new Divide(vx, vx)) + .advanced("(2 + 1)", "2 + 1", x -> 2 + 1, new Add(c2, c1)) + .advanced("(x - 1)", "x - 1", x -> x - 1, new Subtract(vx, c1)) + .advanced("(1 * 2)", "1 * 2", x -> 1 * 2, new Multiply(c1, c2)) + .advanced("(x / 1)", "x / 1", x -> x / 1, new Divide(vx, c1)) + .advanced( + "(1 + (2 + 1))", + "1 + 2 + 1", + x -> 1 + 2 + 1, + new Add(c1, new Add(c2, c1)) + ) + .advanced( + "(x - (x - 1))", + "x - (x - 1)", + x -> x - (x - 1), + new Subtract(vx, new Subtract(vx, c1)) + ) + .advanced( + "(2 * (x / 1))", + "2 * (x / 1)", + x -> 2 * (x / 1), + new Multiply(c2, new Divide(vx, c1)) + ) + .advanced( + "(2 / (x - 1))", + "2 / (x - 1)", + x -> 2 / (x - 1), + new Divide(c2, new Subtract(vx, c1)) + ) + .advanced( + "((1 * 2) + x)", + "1 * 2 + x", + x -> 1 * 2 + x, + new Add(new Multiply(c1, c2), vx) + ) + .advanced( + "((x - 1) - 2)", + "x - 1 - 2", + x -> x - 1 - 2, + new Subtract(new Subtract(vx, c1), c2) + ) + .advanced( + "((x / 1) * 2)", + "x / 1 * 2", + x -> (x / 1) * 2, + new Multiply(new Divide(vx, c1), c2) + ) + .advanced( + "((2 + 1) / 1)", + "(2 + 1) / 1", + x -> (2 + 1) / 1, + new Divide(new Add(c2, c1), c1) + ) + .advanced( + "(1 + (1 + (2 + 1)))", + "1 + 1 + 2 + 1", + x -> 1 + 1 + 2 + 1, + new Add(c1, new Add(c1, new Add(c2, c1))) + ) + .advanced( + "(x - ((1 * 2) + x))", + "x - (1 * 2 + x)", + x -> x - (1 * 2 + x), + new Subtract(vx, new Add(new Multiply(c1, c2), vx)) + ) + .advanced( + "(x * (2 / (x - 1)))", + "x * (2 / (x - 1))", + x -> x * (2 / (x - 1)), + new Multiply(vx, new Divide(c2, new Subtract(vx, c1))) + ) + .advanced( + "(x / (1 + (2 + 1)))", + "x / (1 + 2 + 1)", + x -> x / (1 + 2 + 1), + new Divide(vx, new Add(c1, new Add(c2, c1))) + ) + .advanced( + "((1 * 2) + (2 + 1))", + "1 * 2 + 2 + 1", + x -> 1 * 2 + 2 + 1, + new Add(new Multiply(c1, c2), new Add(c2, c1)) + ) + .advanced( + "((2 + 1) - (2 + 1))", + "2 + 1 - (2 + 1)", + x -> 2 + 1 - (2 + 1), + new Subtract(new Add(c2, c1), new Add(c2, c1)) + ) + .advanced( + "((x - 1) * (x / 1))", + "(x - 1) * (x / 1)", + x -> (x - 1) * (x / 1), + new Multiply(new Subtract(vx, c1), new Divide(vx, c1)) + ) + .advanced( + "((x - 1) / (1 * 2))", + "(x - 1) / (1 * 2)", + x -> (x - 1) / (1 * 2), + new Divide(new Subtract(vx, c1), new Multiply(c1, c2)) + ) + .advanced( + "(((x - 1) - 2) + x)", + "x - 1 - 2 + x", + x -> x - 1 - 2 + x, + new Add(new Subtract(new Subtract(vx, c1), c2), vx) + ) + .advanced( + "(((1 * 2) + x) - 1)", + "1 * 2 + x - 1", + x -> 1 * 2 + x - 1, + new Subtract(new Add(new Multiply(c1, c2), vx), c1) + ) + .advanced( + "(((2 + 1) / 1) * x)", + "(2 + 1) / 1 * x", + x -> ((2 + 1) / 1) * x, + new Multiply(new Divide(new Add(c2, c1), c1), vx) + ) + .advanced( + "((2 / (x - 1)) / 2)", + "2 / (x - 1) / 2", + x -> 2 / (x - 1) / 2, + new Divide(new Divide(c2, new Subtract(vx, c1)), c2) + ); + } +} diff --git a/java/expression/ExpressionTest.java b/java/expression/ExpressionTest.java new file mode 100644 index 0000000..990a5cb --- /dev/null +++ b/java/expression/ExpressionTest.java @@ -0,0 +1,30 @@ +package expression; + +import base.Selector; +import base.TestCounter; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ExpressionTest { + + public static final Selector SELECTOR = new Selector( + ExpressionTest.class, + "easy", + "hard" + ).variant("Base", v(Expression::tester)); + + private ExpressionTest() {} + + public static Consumer v( + final Function> tester + ) { + return t -> tester.apply(t).test(); + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/expression/ExpressionTester.java b/java/expression/ExpressionTester.java new file mode 100644 index 0000000..6de3e61 --- /dev/null +++ b/java/expression/ExpressionTester.java @@ -0,0 +1,415 @@ +package expression; + +import static base.Asserts.assertTrue; + +import base.*; +import expression.common.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ExpressionTester extends Tester { + + private final List VALUES = IntStream.rangeClosed(-10, 10) + .boxed() + .toList(); + private final ExpressionKind kind; + + private final List basic = new ArrayList<>(); + private final List advanced = new ArrayList<>(); + private final Set used = new HashSet<>(); + private final GeneratorBuilder generator; + + private final List> prev = new ArrayList<>(); + private final Map mappings; + + protected ExpressionTester( + final TestCounter counter, + final ExpressionKind kind, + final Function expectedConstant, + final Binary binary, + final BinaryOperator add, + final BinaryOperator sub, + final BinaryOperator mul, + final BinaryOperator div, + final Map mappings + ) { + super(counter); + this.kind = kind; + this.mappings = mappings; + + generator = new GeneratorBuilder( + expectedConstant, + kind::constant, + binary, + kind::randomValue + ); + generator.binary("+", 1600, add, Add.class); + generator.binary("-", 1602, sub, Subtract.class); + generator.binary("*", 2001, mul, Multiply.class); + generator.binary("/", 2002, div, Divide.class); + } + + protected ExpressionTester( + final TestCounter counter, + final ExpressionKind kind, + final Function expectedConstant, + final Binary binary, + final BinaryOperator add, + final BinaryOperator sub, + final BinaryOperator mul, + final BinaryOperator div + ) { + this( + counter, + kind, + expectedConstant, + binary, + add, + sub, + mul, + div, + Map.of() + ); + } + + @Override + public String toString() { + return kind.getName(); + } + + @Override + public void test() { + counter.scope("Basic tests", () -> basic.forEach(Test::test)); + counter.scope("Advanced tests", () -> advanced.forEach(Test::test)); + counter.scope("Random tests", generator::testRandom); + } + + @SuppressWarnings({ "ConstantValue", "EqualsWithItself" }) + private void checkEqualsAndToString( + final String full, + final String mini, + final ToMiniString expression, + final ToMiniString copy + ) { + checkToString("toString", full, expression.toString()); + if (mode() > 0) { + checkToString("toMiniString", mini, expression.toMiniString()); + } + + counter.test(() -> { + assertTrue("Equals to this", expression.equals(expression)); + assertTrue("Equals to copy", expression.equals(copy)); + assertTrue("Equals to null", !expression.equals(null)); + assertTrue("Copy equals to null", !copy.equals(null)); + }); + + final String expressionToString = Objects.requireNonNull( + expression.toString() + ); + for (final Pair pair : prev) { + counter.test(() -> { + final ToMiniString prev = pair.first(); + final String prevToString = pair.second(); + final boolean equals = prevToString.equals(expressionToString); + assertTrue( + "Equals to " + prevToString, + prev.equals(expression) == equals + ); + assertTrue( + "Equals to " + prevToString, + expression.equals(prev) == equals + ); + assertTrue( + "Inconsistent hashCode for " + prev + " and " + expression, + (prev.hashCode() == expression.hashCode()) == equals + ); + }); + } + } + + private void checkToString( + final String method, + final String expected, + final String actual + ) { + counter.test(() -> + assertTrue( + String.format( + "Invalid %s\n expected: %s\n actual: %s", + method, + expected, + actual + ), + expected.equals(actual) + ) + ); + } + + private void check( + final String full, + final E expected, + final E actual, + final List variables, + final List values + ) { + final String vars = IntStream.range(0, variables.size()) + .mapToObj(i -> variables.get(i) + "=" + values.get(i)) + .collect(Collectors.joining(",")); + counter.test(() -> { + final Object expectedResult = evaluate(expected, variables, values); + final Object actualResult = evaluate(actual, variables, values); + final String reason = String.format( + "%s:%n expected `%s`,%n actual `%s`", + String.format("f(%s)\nwhere f is %s", vars, full), + Asserts.toString(expectedResult), + Asserts.toString(actualResult) + ); + if ( + expectedResult != null && + actualResult != null && + expectedResult.getClass() == actualResult.getClass() && + (expectedResult.getClass() == Double.class || + expectedResult.getClass() == Float.class) + ) { + final double expectedValue = ( + (Number) expectedResult + ).doubleValue(); + final double actualValue = ( + (Number) actualResult + ).doubleValue(); + Asserts.assertEquals(reason, expectedValue, actualValue, 1e-6); + } else { + assertTrue( + reason, + Objects.deepEquals(expectedResult, actualResult) + ); + } + }); + } + + private Object evaluate( + final E expression, + final List variables, + final List values + ) { + try { + return kind.evaluate(expression, variables, values); + } catch (final Exception e) { + return e.getClass().getName(); + } + } + + protected ExpressionTester basic( + final String full, + final String mini, + final E expected, + final E actual + ) { + return basicF(full, mini, expected, vars -> actual); + } + + protected ExpressionTester basicF( + final String full, + final String mini, + final E expected, + final Function, E> actual + ) { + return basic(new Test(full, mini, expected, actual)); + } + + private ExpressionTester basic(final Test test) { + Asserts.assertTrue(test.full, used.add(test.full)); + basic.add(test); + return this; + } + + protected ExpressionTester advanced( + final String full, + final String mini, + final E expected, + final E actual + ) { + return advancedF(full, mini, expected, vars -> actual); + } + + protected ExpressionTester advancedF( + final String full, + final String mini, + final E expected, + final Function, E> actual + ) { + Asserts.assertTrue(full, used.add(full)); + advanced.add(new Test(full, mini, expected, actual)); + return this; + } + + protected static Named variable( + final String name, + final E expected + ) { + return Named.of(name, expected); + } + + @FunctionalInterface + public interface Binary { + E apply(BinaryOperator op, E a, E b); + } + + private final class Test { + + private final String full; + private final String mini; + private final E expected; + private final Function, E> actual; + + private Test( + final String full, + final String mini, + final E expected, + final Function, E> actual + ) { + this.full = full; + this.mini = mini; + this.expected = expected; + this.actual = actual; + } + + private void test() { + final List> variables = kind + .variables() + .generate(random(), 3); + final List names = Functional.map(variables, Pair::first); + final E actual = kind.cast(this.actual.apply(names)); + final String full = mangle(this.full, names); + final String mini = mangle(this.mini, names); + + counter.test(() -> { + kind + .allValues(variables.size(), VALUES) + .forEach(values -> + check(mini, expected, actual, names, values) + ); + checkEqualsAndToString(full, mini, actual, actual); + prev.add(Pair.of(actual, full)); + }); + } + + private String mangle(String string, final List names) { + for (int i = 0; i < names.size(); i++) { + string = string.replace("$" + (char) ('x' + i), names.get(i)); + } + for (final Map.Entry mapping : mappings.entrySet()) { + string = string.replace( + mapping.getKey(), + mapping.getValue().toString() + ); + } + return string; + } + } + + private final class GeneratorBuilder { + + private final Generator.Builder generator; + private final NodeRendererBuilder renderer = + new NodeRendererBuilder<>(random()); + private final Renderer.Builder expected; + private final Renderer.Builder actual; + private final Renderer.Builder copy; + private final Binary binary; + + private GeneratorBuilder( + final Function expectedConstant, + final Function actualConstant, + final Binary binary, + final Function randomValue + ) { + generator = Generator.builder( + () -> randomValue.apply(random()), + random() + ); + expected = Renderer.builder(expectedConstant::apply); + actual = Renderer.builder(actualConstant::apply); + copy = Renderer.builder(actualConstant::apply); + + this.binary = binary; + } + + private void binary( + final String name, + final int priority, + final BinaryOperator op, + final Class type + ) { + generator.add(name, 2); + renderer.binary(name, priority); + + expected.binary(name, (unit, a, b) -> binary.apply(op, a, b)); + + @SuppressWarnings("unchecked") + final Constructor constructor = (Constructor< + ? extends E + >) Arrays.stream(type.getConstructors()) + .filter(cons -> Modifier.isPublic(cons.getModifiers())) + .filter(cons -> cons.getParameterCount() == 2) + .findFirst() + .orElseGet(() -> + counter.fail( + "%s(..., ...) constructor not found", + type.getSimpleName() + ) + ); + final Renderer.BinaryOperator actual = (unit, a, b) -> { + try { + return constructor.newInstance(a, b); + } catch (final Exception e) { + return counter.fail(e); + } + }; + this.actual.binary(name, actual); + copy.binary(name, actual); + } + + private void testRandom() { + final NodeRenderer renderer = this.renderer.build(); + final Renderer expectedRenderer = this.expected.build(); + final Renderer actualRenderer = this.actual.build(); + final expression.common.Generator generator = + this.generator.build(kind.variables(), List.of()); + generator.testRandom(counter, 1, expr -> { + final String full = renderer.render(expr, NodeRenderer.FULL); + final String mini = renderer.render(expr, NodeRenderer.MINI); + final E expected = expectedRenderer.render(expr, Unit.INSTANCE); + final E actual = actualRenderer.render(expr, Unit.INSTANCE); + + final List> variables = expr.variables(); + final List names = Functional.map( + variables, + Pair::first + ); + final List values = Stream.generate(() -> + kind.randomValue(random()) + ) + .limit(variables.size()) + .toList(); + + checkEqualsAndToString( + full, + mini, + actual, + copy.build().render(expr, Unit.INSTANCE) + ); + check(full, expected, actual, names, values); + }); + } + } +} diff --git a/java/expression/Floor.java b/java/expression/Floor.java new file mode 100644 index 0000000..edbb6de --- /dev/null +++ b/java/expression/Floor.java @@ -0,0 +1,75 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Floor extends AbstractExpression { + + private final AbstractExpression operand; + + public Floor(AbstractExpression operand) { + this.operand = operand; + } + + private static int floorInt(int n) { + return Math.floorDiv(n, 1000) * 1000; + } + + @Override + public int evaluate(int x) { + return floorInt(operand.evaluate(x)); + } + + @Override + public int evaluate(int x, int y, int z) { + return floorInt(operand.evaluate(x, y, z)); + } + + @Override + public int evaluate(List vars) { + return floorInt(operand.evaluate(vars)); + } + + @Override + public BigInteger evaluateBi(List vars) { + throw new UnsupportedOperationException( + "floor not supported for BigInteger" + ); + } + + @Override + public BigDecimal evaluateBd(List vars) { + throw new UnsupportedOperationException( + "floor not supported for BigDecimal" + ); + } + + @Override + public String toString() { + return "floor(" + operand + ")"; + } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "floor(" + operand.toMiniString() + ")"; + } + return "floor " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Floor)) return false; + return operand.equals(((Floor) obj).operand); + } + + @Override + public int hashCode() { + return operand.hashCode() ^ 0x464C4F52; + } +} diff --git a/java/expression/High.java b/java/expression/High.java new file mode 100644 index 0000000..da9a38a --- /dev/null +++ b/java/expression/High.java @@ -0,0 +1,51 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class High extends AbstractExpression { + private final AbstractExpression operand; + + public High(AbstractExpression operand) { + this.operand = operand; + } + + private static int highestBit(int n) { + if (n == 0) return 0; + n |= (n >> 1); + n |= (n >> 2); + n |= (n >> 4); + n |= (n >> 8); + n |= (n >> 16); + return n - (n >>> 1); + } + + @Override public int evaluate(int x) { return highestBit(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return highestBit(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return highestBit(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { throw new UnsupportedOperationException(); } + @Override public BigDecimal evaluateBd(List vars) { throw new UnsupportedOperationException(); } + + @Override public String toString() { return "high(" + operand + ")"; } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "high(" + operand.toMiniString() + ")"; + } + return "high " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof High)) return false; + return operand.equals(((High) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x48494748; } +} \ No newline at end of file diff --git a/java/expression/ListExpression.java b/java/expression/ListExpression.java new file mode 100644 index 0000000..01cc5e2 --- /dev/null +++ b/java/expression/ListExpression.java @@ -0,0 +1,26 @@ +package expression; + +import base.ExtendedRandom; +import base.Pair; +import expression.common.ExpressionKind; +import expression.common.Type; + +import java.util.List; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +public interface ListExpression extends ToMiniString { + ExpressionKind KIND = new ExpressionKind<>( + new Type<>(a -> a, ExtendedRandom::nextInt, int.class), + ListExpression.class, + (r, c) -> IntStream.range(0, c) + .mapToObj(name -> Pair.of("$" + name, new Variable(name))) + .toList(), + (expr, variables, values) -> expr.evaluate(values) + ); + + int evaluate(List variables); +} diff --git a/java/expression/Log.java b/java/expression/Log.java new file mode 100644 index 0000000..afc219b --- /dev/null +++ b/java/expression/Log.java @@ -0,0 +1,46 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Log extends AbstractBinaryOperation { + public Log(AbstractExpression l, AbstractExpression r) { super(l, r); } + + @Override protected String getOperator() { return "//"; } + @Override protected int getPriority() { return 3; } + @Override protected boolean isRightAssoc() { return true; } + + @Override + protected int applyInt(int a, int b) { + if (a <= 0) throw new ArithmeticException("logarithm of non-positive number"); + if (b <= 1) throw new ArithmeticException("logarithm base must be > 1"); + int result = 0; + int power = 1; + while (power <= a / b) { + power *= b; + result++; + } + return result; + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + if (a.signum() <= 0) throw new ArithmeticException("logarithm of non-positive number"); + if (b.compareTo(BigInteger.TWO) < 0) throw new ArithmeticException("logarithm base must be > 1"); + int result = 0; + BigInteger power = BigInteger.ONE; + while (power.multiply(b).compareTo(a) <= 0) { + power = power.multiply(b); + result++; + } + return BigInteger.valueOf(result); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + throw new UnsupportedOperationException("log not supported for BigDecimal"); + } +} \ No newline at end of file diff --git a/java/expression/Log2.java b/java/expression/Log2.java new file mode 100644 index 0000000..f972e47 --- /dev/null +++ b/java/expression/Log2.java @@ -0,0 +1,52 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Log2 extends AbstractExpression { + private final AbstractExpression operand; + + public Log2(AbstractExpression operand) { + this.operand = operand; + } + + private static int log2(int n) { + if (n <= 0) throw new ArithmeticException("log₂ of non-positive number: " + n); + int result = 0; + int v = n; + while (v > 1) { + v >>= 1; + result++; + } + return result; + } + + @Override public int evaluate(int x) { return log2(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return log2(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return log2(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { throw new UnsupportedOperationException(); } + @Override public BigDecimal evaluateBd(List vars) { throw new UnsupportedOperationException(); } + + @Override public String toString() { return "log₂(" + operand + ")"; } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "log₂(" + operand.toMiniString() + ")"; + } + return "log₂ " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Log2)) return false; + return operand.equals(((Log2) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x4C4F4732; } +} \ No newline at end of file diff --git a/java/expression/Low.java b/java/expression/Low.java new file mode 100644 index 0000000..4b18c02 --- /dev/null +++ b/java/expression/Low.java @@ -0,0 +1,41 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Low extends AbstractExpression { + private final AbstractExpression operand; + + public Low(AbstractExpression operand) { + this.operand = operand; + } + + @Override public int evaluate(int x) { int n = operand.evaluate(x); return n & -n; } + @Override public int evaluate(int x, int y, int z) { int n = operand.evaluate(x,y,z); return n & -n; } + @Override public int evaluate(List vars) { int n = operand.evaluate(vars); return n & -n; } + @Override public BigInteger evaluateBi(List vars) { throw new UnsupportedOperationException(); } + @Override public BigDecimal evaluateBd(List vars) { throw new UnsupportedOperationException(); } + + @Override public String toString() { return "low(" + operand + ")"; } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "low(" + operand.toMiniString() + ")"; + } + return "low " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Low)) return false; + return operand.equals(((Low) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x4C4F5700; } +} \ No newline at end of file diff --git a/java/expression/Max.java b/java/expression/Max.java new file mode 100644 index 0000000..57f24a2 --- /dev/null +++ b/java/expression/Max.java @@ -0,0 +1,44 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Max extends AbstractBinaryOperation { + + public Max(AbstractExpression l, AbstractExpression r) { + super(l, r); + } + + @Override + protected String getOperator() { + return "max"; + } + + @Override + protected int getPriority() { + return 0; + } + + @Override + protected boolean isRightAssoc() { + return false; + } + + @Override + protected int applyInt(int a, int b) { + return Math.max(a, b); + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + return a.max(b); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + return a.max(b); + } +} diff --git a/java/expression/Min.java b/java/expression/Min.java new file mode 100644 index 0000000..c7af478 --- /dev/null +++ b/java/expression/Min.java @@ -0,0 +1,44 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Min extends AbstractBinaryOperation { + + public Min(AbstractExpression l, AbstractExpression r) { + super(l, r); + } + + @Override + protected String getOperator() { + return "min"; + } + + @Override + protected int getPriority() { + return 0; + } + + @Override + protected boolean isRightAssoc() { + return false; + } + + @Override + protected int applyInt(int a, int b) { + return Math.min(a, b); + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + return a.min(b); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + return a.min(b); + } +} diff --git a/java/expression/Multiply.java b/java/expression/Multiply.java new file mode 100644 index 0000000..36925cb --- /dev/null +++ b/java/expression/Multiply.java @@ -0,0 +1,44 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Multiply extends AbstractBinaryOperation { + + public Multiply(AbstractExpression l, AbstractExpression r) { + super(l, r); + } + + @Override + protected String getOperator() { + return "*"; + } + + @Override + protected int getPriority() { + return 2; + } + + @Override + protected boolean isRightAssoc() { + return false; + } + + @Override + protected int applyInt(int a, int b) { + return a * b; + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + return a.multiply(b); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + return a.multiply(b); + } +} diff --git a/java/expression/OverflowException.java b/java/expression/OverflowException.java new file mode 100644 index 0000000..da9799c --- /dev/null +++ b/java/expression/OverflowException.java @@ -0,0 +1,10 @@ +package expression; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class OverflowException extends ArithmeticException { + public OverflowException(String operation) { + super("overflow in " + operation); + } +} \ No newline at end of file diff --git a/java/expression/Pow2.java b/java/expression/Pow2.java new file mode 100644 index 0000000..2dba043 --- /dev/null +++ b/java/expression/Pow2.java @@ -0,0 +1,47 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Pow2 extends AbstractExpression { + private final AbstractExpression operand; + + public Pow2(AbstractExpression operand) { + this.operand = operand; + } + + private static int pow2(int n) { + if (n < 0) throw new ArithmeticException("pow₂ of negative number: " + n); + if (n >= 31) throw new OverflowException("pow₂"); + return 1 << n; + } + + @Override public int evaluate(int x) { return pow2(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return pow2(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return pow2(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { throw new UnsupportedOperationException(); } + @Override public BigDecimal evaluateBd(List vars) { throw new UnsupportedOperationException(); } + + @Override public String toString() { return "pow₂(" + operand + ")"; } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "pow₂(" + operand.toMiniString() + ")"; + } + return "pow₂ " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Pow2)) return false; + return operand.equals(((Pow2) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x504F5732; } +} \ No newline at end of file diff --git a/java/expression/Power.java b/java/expression/Power.java new file mode 100644 index 0000000..f3935c2 --- /dev/null +++ b/java/expression/Power.java @@ -0,0 +1,46 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Power extends AbstractBinaryOperation { + public Power(AbstractExpression l, AbstractExpression r) { super(l, r); } + + @Override protected String getOperator() { return "**"; } + @Override protected int getPriority() { return 3; } + @Override protected boolean isRightAssoc() { return true; } + + @Override + protected int applyInt(int base, int exp) { + if (exp < 0) throw new ArithmeticException("negative exponent"); + if (base == 0 && exp == 0) throw new ArithmeticException("zero to the power of zero"); + if (exp == 0) return 1; + if (base == 0) return 0; + int result = 1; + int b = base; + int e = exp; + while (e > 0) { + if ((e & 1) == 1) { + result = checkedMul(result, b); + } + if (e > 1) b = checkedMul(b, b); + e >>= 1; + } + return result; + } + + private static int checkedMul(int a, int b) { + if (a == 0 || b == 0) return 0; + if (a == Integer.MIN_VALUE && b == -1) throw new OverflowException("power"); + if (b == Integer.MIN_VALUE && a == -1) throw new OverflowException("power"); + int result = a * b; + if (result / a != b) throw new OverflowException("power"); + return result; + } + + @Override protected BigInteger applyBi(BigInteger a, BigInteger b) { return a.pow(b.intValueExact()); } + @Override protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { return a.pow(b.intValueExact()); } +} \ No newline at end of file diff --git a/java/expression/Reverse.java b/java/expression/Reverse.java new file mode 100644 index 0000000..eaacdf1 --- /dev/null +++ b/java/expression/Reverse.java @@ -0,0 +1,84 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Reverse extends AbstractExpression { + + private final AbstractExpression operand; + + public Reverse(AbstractExpression operand) { + this.operand = operand; + } + + private static int reverseInt(int n) { + boolean negative = n < 0; + long abs = Math.abs((long) n); + long reversed = 0; + while (abs > 0) { + reversed = reversed * 10 + (abs % 10); + abs /= 10; + } + long result = negative ? -reversed : reversed; + return (int) result; + } + + @Override + public int evaluate(int x) { + return reverseInt(operand.evaluate(x)); + } + + @Override + public int evaluate(int x, int y, int z) { + return reverseInt(operand.evaluate(x, y, z)); + } + + @Override + public int evaluate(List vars) { + return reverseInt(operand.evaluate(vars)); + } + + @Override + public BigInteger evaluateBi(List vars) { + throw new UnsupportedOperationException( + "reverse not supported for BigInteger" + ); + } + + @Override + public BigDecimal evaluateBd(List vars) { + throw new UnsupportedOperationException( + "reverse not supported for BigDecimal" + ); + } + + @Override + public String toString() { + return "reverse(" + operand + ")"; + } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "reverse(" + operand.toMiniString() + ")"; + } + + return "reverse " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Reverse)) return false; + return operand.equals(((Reverse) obj).operand); + } + + @Override + public int hashCode() { + return operand.hashCode() ^ 0x52455645; + } +} diff --git a/java/expression/SetBit.java b/java/expression/SetBit.java new file mode 100644 index 0000000..78c07d8 --- /dev/null +++ b/java/expression/SetBit.java @@ -0,0 +1,46 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class SetBit extends AbstractBinaryOperation { + + public SetBit(AbstractExpression l, AbstractExpression r) { + super(l, r); + } + + @Override + protected String getOperator() { + return "set"; + } + + @Override + protected int getPriority() { + return 0; + } + + @Override + protected boolean isRightAssoc() { + return true; + } + + @Override + protected int applyInt(int a, int b) { + return a | (1 << b); + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + return a.setBit(b.intValueExact()); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + throw new UnsupportedOperationException( + "set not supported for BigDecimal" + ); + } +} diff --git a/java/expression/Sqrt.java b/java/expression/Sqrt.java new file mode 100644 index 0000000..1fcc65e --- /dev/null +++ b/java/expression/Sqrt.java @@ -0,0 +1,60 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Sqrt extends AbstractExpression { + private final AbstractExpression operand; + + public Sqrt(AbstractExpression operand) { + this.operand = operand; + } + + private static int sqrtInt(int n) { + if (n < 0) throw new ArithmeticException("sqrt of negative number: " + n); + if (n == 0) return 0; + // Binary search: floor(sqrt(n)), result <= 46340 (sqrt(MAX_INT) < 46341) + int lo = 0, hi = 46340; + while (lo < hi) { + int mid = (lo + hi + 1) / 2; + if (mid * mid <= n) { + lo = mid; + } else { + hi = mid - 1; + } + } + return lo; + } + + @Override public int evaluate(int x) { return sqrtInt(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return sqrtInt(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return sqrtInt(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { return operand.evaluateBi(vars).sqrt(); } + @Override public BigDecimal evaluateBd(List vars) { + throw new UnsupportedOperationException("sqrt not supported for BigDecimal"); + } + + @Override public String toString() { return "√(" + operand + ")"; } + // @Override public String toMiniString() { return "√" + operand.toMiniString(); } + + @Override + public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) { + return "√(" + operand.toMiniString() + ")"; + } + return "√" + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Sqrt)) return false; + return operand.equals(((Sqrt) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x53515254; } +} \ No newline at end of file diff --git a/java/expression/Square.java b/java/expression/Square.java new file mode 100644 index 0000000..29636f0 --- /dev/null +++ b/java/expression/Square.java @@ -0,0 +1,51 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Square extends AbstractExpression { + private final AbstractExpression operand; + + public Square(AbstractExpression operand) { + this.operand = operand; + } + + private static int squareInt(int n) { + if (n == Integer.MIN_VALUE) throw new OverflowException("square"); + int result = n * n; + // overflow check: result / n should == n (if n != 0) + if (n != 0 && result / n != n) throw new OverflowException("square"); + return result; + } + + @Override public int evaluate(int x) { return squareInt(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return squareInt(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return squareInt(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { return operand.evaluateBi(vars).pow(2); } + @Override public BigDecimal evaluateBd(List vars) { return operand.evaluateBd(vars).pow(2); } + + @Override public String toString() { return "(" + operand + ")²"; } + + @Override + public String toMiniString() { + String s = operand.toMiniString(); + boolean needParens = operand instanceof AbstractBinaryOperation + || operand instanceof Cbrt + || operand instanceof Sqrt + || (s.startsWith("-") && (s.length() < 2 || !Character.isDigit(s.charAt(1)))); + return needParens ? "(" + s + ")²" : s + "²"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Square)) return false; + return operand.equals(((Square) obj).operand); + } + + @Override public int hashCode() { return operand.hashCode() ^ 0x53515232; } +} \ No newline at end of file diff --git a/java/expression/Subtract.java b/java/expression/Subtract.java new file mode 100644 index 0000000..5ad4848 --- /dev/null +++ b/java/expression/Subtract.java @@ -0,0 +1,44 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Subtract extends AbstractBinaryOperation { + + public Subtract(AbstractExpression l, AbstractExpression r) { + super(l, r); + } + + @Override + protected String getOperator() { + return "-"; + } + + @Override + protected int getPriority() { + return 1; + } + + @Override + protected boolean isRightAssoc() { + return true; + } + + @Override + protected int applyInt(int a, int b) { + return a - b; + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + return a.subtract(b); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + return a.subtract(b); + } +} diff --git a/java/expression/ToMiniString.java b/java/expression/ToMiniString.java new file mode 100644 index 0000000..60a655c --- /dev/null +++ b/java/expression/ToMiniString.java @@ -0,0 +1,10 @@ +package expression; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface ToMiniString { + default String toMiniString() { + return toString(); + } +} diff --git a/java/expression/TripleExpression.java b/java/expression/TripleExpression.java new file mode 100644 index 0000000..d48010a --- /dev/null +++ b/java/expression/TripleExpression.java @@ -0,0 +1,317 @@ +package expression; + +import base.ExtendedRandom; +import base.Pair; +import base.Selector; +import base.TestCounter; +import expression.common.ExpressionKind; +import expression.common.Type; +import java.util.List; + +/** + * Three-argument arithmetic expression over integers. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +@SuppressWarnings("ClassReferencesSubclass") +public interface TripleExpression extends ToMiniString { + int evaluate(int x, int y, int z); + + // Tests follow. You may temporarily remove everything til the end. + + Type TYPE = new Type<>(a -> a, ExtendedRandom::nextInt, int.class); + ExpressionKind KIND = new ExpressionKind<>( + TYPE, + TripleExpression.class, + List.of( + Pair.of("x", new Variable("x")), + Pair.of("y", new Variable("y")), + Pair.of("z", new Variable("z")) + ), + (expr, variables, values) -> + expr.evaluate(values.get(0), values.get(1), values.get(2)) + ); + + @SuppressWarnings("PointlessArithmeticExpression") + static ExpressionTester tester(final TestCounter counter) { + final Variable vx = new Variable("x"); + final Variable vy = new Variable("y"); + final Variable vz = new Variable("z"); + + return new ExpressionTester<>( + counter, + KIND, + c -> (x, y, z) -> c, + (op, a, b) -> + (x, y, z) -> op.apply(a.evaluate(x, y, z), b.evaluate(x, y, z)), + Integer::sum, + (a, b) -> a - b, + (a, b) -> a * b, + (a, b) -> a / b + ) + .basic("10", "10", (x, y, z) -> 10, c(10)) + .basic("x", "x", (x, y, z) -> x, vx) + .basic("y", "y", (x, y, z) -> y, vy) + .basic("z", "z", (x, y, z) -> z, vz) + .basic("(x + 2)", "x + 2", (x, y, z) -> x + 2, new Add(vx, c(2))) + .basic( + "(2 - y)", + "2 - y", + (x, y, z) -> 2 - y, + new Subtract(c(2), vy) + ) + .basic( + "(3 * z)", + "3 * z", + (x, y, z) -> 3 * z, + new Multiply(c(3), vz) + ) + .basic( + "(x / -2)", + "x / -2", + (x, y, z) -> -x / 2, + new Divide(vx, c(-2)) + ) + .basic( + "((1 + 2) + 3)", + "1 + 2 + 3", + (x, y, z) -> 6, + new Add(new Add(c(1), c(2)), c(3)) + ) + .basic( + "(1 + (2 + 3))", + "1 + 2 + 3", + (x, y, z) -> 6, + new Add(c(1), new Add(c(2), c(3))) + ) + .basic( + "((1 - 2) - 3)", + "1 - 2 - 3", + (x, y, z) -> -4, + new Subtract(new Subtract(c(1), c(2)), c(3)) + ) + .basic( + "(1 - (2 - 3))", + "1 - (2 - 3)", + (x, y, z) -> 2, + new Subtract(c(1), new Subtract(c(2), c(3))) + ) + .basic( + "((1 * 2) * 3)", + "1 * 2 * 3", + (x, y, z) -> 6, + new Multiply(new Multiply(c(1), c(2)), c(3)) + ) + .basic( + "(1 * (2 * 3))", + "1 * 2 * 3", + (x, y, z) -> 6, + new Multiply(c(1), new Multiply(c(2), c(3))) + ) + .basic( + "((10 / 2) / 3)", + "10 / 2 / 3", + (x, y, z) -> 10 / 2 / 3, + new Divide(new Divide(c(10), c(2)), c(3)) + ) + .basic( + "(10 / (3 / 2))", + "10 / (3 / 2)", + (x, y, z) -> 10, + new Divide(c(10), new Divide(c(3), c(2))) + ) + .basic( + "((x * y) + ((z - 1) / 10))", + "x * y + (z - 1) / 10", + (x, y, z) -> x * y + (z - 1) / 10, + new Add( + new Multiply(vx, vy), + new Divide(new Subtract(vz, c(1)), c(10)) + ) + ) + .basic("(x + y)", "x + y", (x, y, z) -> x + y, new Add(vx, vy)) + .basic("(y + x)", "y + x", (x, y, z) -> y + x, new Add(vy, vx)) + .advanced( + "(1 + 1)", + "1 + 1", + (x, y, z) -> 1 + 1, + new Add(c(1), c(1)) + ) + .advanced( + "(y - x)", + "y - x", + (x, y, z) -> y - x, + new Subtract(vy, vx) + ) + .advanced( + "(2 * x)", + "2 * x", + (x, y, z) -> 2 * x, + new Multiply(c(2), vx) + ) + .advanced( + "(2 / x)", + "2 / x", + (x, y, z) -> 2 / x, + new Divide(c(2), vx) + ) + .advanced( + "(z + (1 + 1))", + "z + 1 + 1", + (x, y, z) -> z + 1 + 1, + new Add(vz, new Add(c(1), c(1))) + ) + .advanced( + "(2 - (y - x))", + "2 - (y - x)", + (x, y, z) -> 2 - (y - x), + new Subtract(c(2), new Subtract(vy, vx)) + ) + .advanced( + "(z * (2 / x))", + "z * (2 / x)", + (x, y, z) -> z * (2 / x), + new Multiply(vz, new Divide(c(2), vx)) + ) + .advanced( + "(z / (y - x))", + "z / (y - x)", + (x, y, z) -> z / (y - x), + new Divide(vz, new Subtract(vy, vx)) + ) + .advanced( + "((2 * x) + y)", + "2 * x + y", + (x, y, z) -> 2 * x + y, + new Add(new Multiply(c(2), vx), vy) + ) + .advanced( + "((y - x) - 2)", + "y - x - 2", + (x, y, z) -> y - x - 2, + new Subtract(new Subtract(vy, vx), c(2)) + ) + .advanced( + "((2 / x) * y)", + "2 / x * y", + (x, y, z) -> (2 / x) * y, + new Multiply(new Divide(c(2), vx), vy) + ) + .advanced( + "((1 + 1) / x)", + "(1 + 1) / x", + (x, y, z) -> (1 + 1) / x, + new Divide(new Add(c(1), c(1)), vx) + ) + .advanced( + "(1 + (2 * 3))", + "1 + 2 * 3", + (x, y, z) -> 7, + new Add(c(1), new Multiply(c(2), c(3))) + ) + .advanced( + "(1 - (2 * 3))", + "1 - 2 * 3", + (x, y, z) -> -5, + new Subtract(c(1), new Multiply(c(2), c(3))) + ) + .advanced( + "(1 + (2 / 3))", + "1 + 2 / 3", + (x, y, z) -> 1, + new Add(c(1), new Divide(c(2), c(3))) + ) + .advanced( + "(1 - (2 / 3))", + "1 - 2 / 3", + (x, y, z) -> 1, + new Subtract(c(1), new Divide(c(2), c(3))) + ) + .advanced( + "(2 + (z + (1 + 1)))", + "2 + z + 1 + 1", + (x, y, z) -> 2 + z + 1 + 1, + new Add(c(2), new Add(vz, new Add(c(1), c(1)))) + ) + .advanced( + "(1 - ((2 * x) + y))", + "1 - (2 * x + y)", + (x, y, z) -> 1 - (2 * x + y), + new Subtract(c(1), new Add(new Multiply(c(2), vx), vy)) + ) + .advanced( + "(1 * (z / (y - x)))", + "1 * (z / (y - x))", + (x, y, z) -> 1 * (z / (y - x)), + new Multiply(c(1), new Divide(vz, new Subtract(vy, vx))) + ) + .advanced( + "(z / (z + (1 + 1)))", + "z / (z + 1 + 1)", + (x, y, z) -> z / (z + 1 + 1), + new Divide(vz, new Add(vz, new Add(c(1), c(1)))) + ) + .advanced( + "((2 * x) + (1 + 1))", + "2 * x + 1 + 1", + (x, y, z) -> 2 * x + 1 + 1, + new Add(new Multiply(c(2), vx), new Add(c(1), c(1))) + ) + .advanced( + "((1 + 1) - (1 + 1))", + "1 + 1 - (1 + 1)", + (x, y, z) -> 1 + 1 - (1 + 1), + new Subtract(new Add(c(1), c(1)), new Add(c(1), c(1))) + ) + .advanced( + "((y - x) * (2 / x))", + "(y - x) * (2 / x)", + (x, y, z) -> (y - x) * (2 / x), + new Multiply(new Subtract(vy, vx), new Divide(c(2), vx)) + ) + .advanced( + "((y - x) / (2 * x))", + "(y - x) / (2 * x)", + (x, y, z) -> (y - x) / (2 * x), + new Divide(new Subtract(vy, vx), new Multiply(c(2), vx)) + ) + .advanced( + "(((y - x) - 2) + 1)", + "y - x - 2 + 1", + (x, y, z) -> y - x - 2 + 1, + new Add(new Subtract(new Subtract(vy, vx), c(2)), c(1)) + ) + .advanced( + "(((2 * x) + y) - z)", + "2 * x + y - z", + (x, y, z) -> 2 * x + y - z, + new Subtract(new Add(new Multiply(c(2), vx), vy), vz) + ) + .advanced( + "(((1 + 1) / x) * 2)", + "(1 + 1) / x * 2", + (x, y, z) -> ((1 + 1) / x) * 2, + new Multiply(new Divide(new Add(c(1), c(1)), vx), c(2)) + ) + .advanced( + "((z / (y - x)) / x)", + "z / (y - x) / x", + (x, y, z) -> z / (y - x) / x, + new Divide(new Divide(vz, new Subtract(vy, vx)), vx) + ); + } + + private static Const c(final Integer c) { + return TYPE.constant(c); + } + + Selector SELECTOR = ExpressionTest.SELECTOR.variant( + "Triple", + ExpressionTest.v(TripleExpression::tester) + ); + + static void main(final String... args) { + TripleExpression.SELECTOR.main(args); + } +} diff --git a/java/expression/Variable.java b/java/expression/Variable.java new file mode 100644 index 0000000..e9f1837 --- /dev/null +++ b/java/expression/Variable.java @@ -0,0 +1,118 @@ +package expression; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class Variable extends AbstractExpression { + + private final String name; + private final int index; + + public Variable(String name) { + this.name = name; + this.index = -1; + } + + public Variable(int index) { + this.index = index; + this.name = "$" + index; + } + + public Variable(int index, String name) { + this.index = index; + this.name = name; + } + + @Override + public int evaluate(int x) { + if (index >= 0) { + if (index == 0) return x; + throw new IllegalStateException( + "Positional variable $" + + index + + " cannot be evaluated with a single value" + ); + } + if ("x".equals(name)) return x; + throw new IllegalStateException( + "Variable '" + name + "' is not 'x'; use evaluate(x, y, z) instead" + ); + } + + @Override + public int evaluate(int x, int y, int z) { + if (index >= 0) { + return switch (index) { + case 0 -> x; + case 1 -> y; + case 2 -> z; + default -> throw new IndexOutOfBoundsException( + "Variable index " + + index + + " out of range for triple evaluate" + ); + }; + } + return switch (name) { + case "x" -> x; + case "y" -> y; + case "z" -> z; + default -> throw new IllegalStateException( + "Unknown variable: " + name + ); + }; + } + + @Override + public int evaluate(List vars) { + return vars.get(resolvedIndex()); + } + + @Override + public BigInteger evaluateBi(List vars) { + return vars.get(resolvedIndex()); + } + + @Override + public BigDecimal evaluateBd(List vars) { + return vars.get(resolvedIndex()); + } + + private int resolvedIndex() { + if (index >= 0) return index; + return switch (name) { + case "x" -> 0; + case "y" -> 1; + case "z" -> 2; + default -> throw new IllegalStateException( + "Unknown variable: " + name + ); + }; + } + + @Override + public String toString() { + return name; + } + + @Override + public String toMiniString() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Variable)) return false; + return name.equals(((Variable) obj).name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } +} diff --git a/java/expression/common/Expr.java b/java/expression/common/Expr.java new file mode 100644 index 0000000..b1dd418 --- /dev/null +++ b/java/expression/common/Expr.java @@ -0,0 +1,32 @@ +package expression.common; + +import base.Functional; +import base.Pair; + +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public record Expr(Node node, List> variables) { + public List> variables(final BiFunction f) { + return Functional.map( + variables, + variable -> variable.second(f.apply(variable.first(), variable.second())) + ); + } + + public Expr convert(final BiFunction f) { + return of(node, variables(f)); + } + + public Expr node(final Function, Node> f) { + return of(f.apply(node), variables); + } + + public static Expr of(final Node node, final List> variables) { + return new Expr<>(node, variables); + } +} diff --git a/java/expression/common/ExpressionKind.java b/java/expression/common/ExpressionKind.java new file mode 100644 index 0000000..d8da0aa --- /dev/null +++ b/java/expression/common/ExpressionKind.java @@ -0,0 +1,94 @@ +package expression.common; + +import base.ExtendedRandom; +import base.Functional; +import base.Pair; +import expression.ToMiniString; + +import java.util.List; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ExpressionKind { + private final Type type; + private final Class kind; + private final Variables variables; + private final Evaluator evaluator; + + public ExpressionKind( + final Type type, + final Class kind, + final Variables variables, + final Evaluator evaluator + ) { + this.type = type; + this.kind = kind; + this.variables = variables; + this.evaluator = evaluator; + } + + public ExpressionKind( + final Type type, + final Class kind, + final List> variables, + final Evaluator evaluator + ) { + this(type, kind, (r, c) -> variables, evaluator); + } + + public C evaluate(final E expression, final List variables, final List values) throws Exception { + return evaluator.evaluate(expression, variables, values); + } + + public E cast(final Object expression) { + return kind.cast(expression); + } + + public String getName() { + return kind.getSimpleName(); + } + + public E constant(final C value) { + return cast(type.constant(value)); + } + + public C randomValue(final ExtendedRandom random) { + return type.randomValue(random); + } + + public List> allValues(final int length, final List values) { + return Functional.allValues(fromInts(values), length); + } + + public List fromInts(final List values) { + return Functional.map(values, this::fromInt); + } + + public C fromInt(final int value) { + return type.fromInt(value); + } + + @Override + public String toString() { + return kind.getName(); + } + + public ExpressionKind withVariables(final Variables variables) { + return new ExpressionKind<>(type, kind, variables, evaluator); + } + + public Variables variables() { + return variables; + } + + @FunctionalInterface + public interface Variables { + List> generate(final ExtendedRandom random, final int count); + } + + @FunctionalInterface + public interface Evaluator { + R evaluate(final E expression, final List vars, final List values) throws Exception; + } +} diff --git a/java/expression/common/Generator.java b/java/expression/common/Generator.java new file mode 100644 index 0000000..2b7a52d --- /dev/null +++ b/java/expression/common/Generator.java @@ -0,0 +1,173 @@ +package expression.common; + +import base.*; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class Generator { + private final Supplier constant; + private final List> ops; + private final ExpressionKind.Variables variables; + private final Set forbidden; + private final ExtendedRandom random; + private final List>, Stream>>> basicTests; + + public Generator( + final Supplier constant, + final List> ops, + final ExpressionKind.Variables variables, + final Set forbidden, + final ExtendedRandom random, + final List>, Stream>>> basicTests + ) { + this.constant = constant; + this.ops = List.copyOf(ops); + this.variables = variables; + this.forbidden = Set.copyOf(forbidden); + this.random = random; + this.basicTests = List.copyOf(basicTests); + } + + public static Builder builder(final Supplier constant, final ExtendedRandom random) { + return new Builder<>(random, constant); + } + + public void testRandom( + final TestCounter counter, + final int denominator, + final Consumer> consumer + ) { + final int d = Math.max(TestCounter.DENOMINATOR, denominator); + testRandom(counter, consumer, 1, 100, 100 / d, (vars, depth) -> generateFullDepth(vars, Math.min(depth, 3))); + testRandom(counter, consumer, 2, 1000 / d, 1, this::generateSize); + testRandom(counter, consumer, 3, 12, 100 / d, this::generateFullDepth); + testRandom(counter, consumer, 4, 777 / d, 1, this::generatePartialDepth); + } + + private void testRandom( + final TestCounter counter, + final Consumer> consumer, + final int seq, + final int levels, + final int perLevel, + final BiFunction>, Integer, Node> generator + ) { + counter.scope("Random tests #" + seq, () -> { + final int total = levels * perLevel; + int generated = 0; + for (int level = 0; level < levels; level++) { + for (int j = 0; j < perLevel; j++) { + if (generated % 100 == 0) { + progress(counter, total, generated); + } + generated++; + + final List> vars = variables(random.nextInt(10) + 1); + consumer.accept(Expr.of(generator.apply(Functional.map(vars, v -> Node.op(v.first())), level), vars)); + } + } + progress(counter, generated, total); + }); + } + + private static void progress(final TestCounter counter, final int total, final int generated) { + counter.format("Completed %4d out of %d%n", generated, total); + } + + private Node generate( + final List> variables, + final boolean nullary, + final Supplier> unary, + final Supplier, Node>> binary + ) { + if (nullary || ops.isEmpty()) { + return random.nextBoolean() ? random.randomItem(variables) : Node.constant(constant.get()); + } else { + final Named op = random.randomItem(ops); + if (Math.abs(op.value()) == 1) { + return Node.op(op.name(), (op.value() + 1) >> 1, unary.get()); + } else { + final Pair, Node> pair = binary.get(); + return Node.op(op.name(), pair.first(), pair.second()); + } + } + } + + private Node generate(final List> variables, final boolean nullary, final Supplier> child) { + return generate(variables, nullary, child, () -> Pair.of(child.get(), child.get())); + } + + private Node generateFullDepth(final List> variables, final int depth) { + return generate(variables, depth == 0, () -> generateFullDepth(variables, depth - 1)); + } + + private Node generatePartialDepth(final List> variables, final int depth) { + return generate(variables, depth == 0, () -> generatePartialDepth(variables, random.nextInt(depth))); + } + + private Node generateSize(final List> variables, final int size) { + final int first = size <= 1 ? 0 : random.nextInt(size); + return generate( + variables, + size == 0, + () -> generateSize(variables, size - 1), + () -> Pair.of( + generateSize(variables, first), + generateSize(variables, size - 1 - first) + ) + ); + } + + public void testBasic(final Consumer> consumer) { + basicTests.forEach(test -> { + final List> vars = variables(random.nextInt(5) + 3); + test.apply(Functional.map(vars, v -> Node.op(v.first()))) + .map(node -> Expr.of(node, vars)) + .forEachOrdered(consumer); + }); + } + + public List> variables(final int count) { + List> vars; + do { + vars = variables.generate(random, count); + } while (vars.stream().map(Pair::first).anyMatch(forbidden::contains)); + return vars; + } + + /** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ + public static final class Builder { + private final ExtendedRandom random; + private final Supplier constant; + + private final List> ops = new ArrayList<>(); + private final Set forbidden = new HashSet<>(); + + private Builder(final ExtendedRandom random, final Supplier constant) { + this.random = random; + this.constant = constant; + } + + public void add(final String name, final int arity) { + ops.add(Named.of(name, arity)); + forbidden.add(name); + } + + public Generator build( + final ExpressionKind.Variables variables, + final List>, Stream>>> basicTests + ) { + return new Generator<>(constant, ops, variables, forbidden, random, basicTests); + } + } +} diff --git a/java/expression/common/Node.java b/java/expression/common/Node.java new file mode 100644 index 0000000..df13ff8 --- /dev/null +++ b/java/expression/common/Node.java @@ -0,0 +1,106 @@ +package expression.common; + +import java.util.function.Function; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public abstract class Node { + private Node() { + } + + public abstract R get(Const con, Nullary nul, Unary, R> un, Binary, R> bin); + public abstract R cata(Const con, Nullary nul, Unary un, Binary bin); + + public final String toPolish() { + return cata( + T::toString, + name -> name, + (name, priority, a) -> a + " " + name + ":1", + (name, a1, a2) -> a1 + " " + a2 + " " + name + ":2" + ); + } + + @Override + public final String toString() { + return cata( + T::toString, + name -> name, + (name, priority, a) -> name.equals("[") ? "[" + a + "]" : + (priority & 1) == 1 ? "(" + name + " " + a + ")" : "(" + a + " " + name + ")", + (name, a1, a2) -> "(" + a1 + " " + name + " " + a2 + ")" + ); + } + + public static Node constant(final T value) { + return new Node<>() { + @Override + public R get(final Const con, final Nullary nul, final Unary, R> un, final Binary, R> bin) { + return con.apply(value); + } + + @Override + public R cata(final Const con, final Nullary nul, final Unary un, final Binary bin) { + return con.apply(value); + } + }; + } + + public static Node op(final String name) { + return new Node<>() { + @Override + public R get(final Const con, final Nullary nul, final Unary, R> un, final Binary, R> bin) { + return nul.apply(name); + } + + @Override + public R cata(final Const con, final Nullary nul, final Unary un, final Binary bin) { + return nul.apply(name); + } + }; + } + + public static Node op(final String name, final int priority, final Node arg) { + return new Node<>() { + @Override + public R get(final Const con, final Nullary nul, final Unary, R> un, final Binary, R> bin) { + return un.apply(name, priority, arg); + } + + @Override + public R cata(final Const con, final Nullary nul, final Unary un, final Binary bin) { + return un.apply(name, priority, arg.cata(con, nul, un, bin)); + } + }; + } + + public static Node op(final String name, final Node arg1, final Node arg2) { + return new Node<>() { + @Override + public R get(final Const con, final Nullary nul, final Unary, R> un, final Binary, R> bin) { + return bin.apply(name, arg1, arg2); + } + + @Override + public R cata(final Const con, final Nullary nul, final Unary un, final Binary bin) { + return bin.apply(name, arg1.cata(con, nul, un, bin), arg2.cata(con, nul, un, bin)); + } + }; + } + + @FunctionalInterface + public interface Const extends Function {} + + @FunctionalInterface + public interface Nullary extends Function {} + + @FunctionalInterface + public interface Unary { + R apply(String name, int priority, T arg); + } + + @FunctionalInterface + public interface Binary { + R apply(String name, T arg1, T arg2); + } +} diff --git a/java/expression/common/NodeRenderer.java b/java/expression/common/NodeRenderer.java new file mode 100644 index 0000000..b08878f --- /dev/null +++ b/java/expression/common/NodeRenderer.java @@ -0,0 +1,96 @@ +package expression.common; + +import base.ExtendedRandom; + +import java.util.List; +import java.util.Map; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class NodeRenderer { + public static final String PAREN = "["; + public static final List DEFAULT_PARENS = List.of(paren("(", ")")); + + public static final Mode MINI_MODE = Mode.SIMPLE_MINI; // Replace by TRUE_MINI for some challenge; + public static final Settings FULL = Mode.FULL.settings(0); + public static final Settings FULL_EXTRA = Mode.FULL.settings(Integer.MAX_VALUE / 4); + public static final Settings SAME = Mode.SAME.settings(0); + public static final Settings MINI = MINI_MODE.settings(0); + public static final Settings TRUE_MINI = Mode.TRUE_MINI.settings(0); + + private final Renderer> renderer; + private final Map brackets; + private final ExtendedRandom random; + + public NodeRenderer( + final Renderer> renderer, + final Map brackets, + final ExtendedRandom random + ) { + this.renderer = renderer; + this.brackets = Map.copyOf(brackets); + this.random = random; + } + + public static Node paren(final boolean condition, final Node node) { + return condition ? Node.op(PAREN, 1, node) : node; + } + + public static Paren paren(final String open, final String close) { + return new Paren(open, close); + } + + public Node renderToNode(final Settings settings, final Expr expr) { + final Expr> convert = expr.convert((name, variable) -> Node.op(name)); + return renderer.render(convert, settings); + } + + public String render(final Node node, final List parens) { + return node.cata( + String::valueOf, + name -> name, + (name, priority, arg) -> + name == PAREN ? random.randomItem(parens).apply(arg) : + priority == Integer.MAX_VALUE ? name + arg + brackets.get(name) : + (priority & 1) == 1 ? name + arg : + arg + name, + (name, a, b) -> a + " " + name + " " + b + ); + } + + public String render(final Expr expr, final Settings settings) { + return render(renderToNode(settings, expr), settings.parens()); + } + + public enum Mode { + FULL, SAME, TRUE_MINI, SIMPLE_MINI; + + public Settings settings(final int limit) { + return new Settings(this, limit); + } + } + + public record Paren(String open, String close) { + String apply(final String expression) { + return open() + expression + close(); + } + } + + public record Settings(Mode mode, int limit, List parens) { + public Settings(final Mode mode, final int limit) { + this(mode, limit, DEFAULT_PARENS); + } + + public Node extra(Node node, final ExtendedRandom random) { + while (random.nextInt(Integer.MAX_VALUE) < limit) { + node = paren(true, node); + } + return node; + } + + public Settings withParens(final List parens) { + return this.parens.equals(parens) ? this : new Settings(mode, limit, List.copyOf(parens)); + } + } +} diff --git a/java/expression/common/NodeRendererBuilder.java b/java/expression/common/NodeRendererBuilder.java new file mode 100644 index 0000000..5366414 --- /dev/null +++ b/java/expression/common/NodeRendererBuilder.java @@ -0,0 +1,145 @@ +package expression.common; + +import base.ExtendedRandom; +import base.Functional; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("StaticMethodOnlyUsedInOneClass") +public class NodeRendererBuilder { + private final Renderer.Builder> nodeRenderer = Renderer.builder(Node::constant); + private final Map priorities = new HashMap<>(); + private final Map brackets = new HashMap<>(); + private final ExtendedRandom random; + + public NodeRendererBuilder(final ExtendedRandom random) { + this.random = random; + nodeRenderer.unary(NodeRenderer.PAREN, (mode, arg) -> NodeRenderer.paren(true, arg)); + } + + public void unary(final String name, final int priority) { + final String space = name.equals("-") || Character.isLetter(name.charAt(0)) ? " " : ""; + nodeRenderer.unary( + name, + (settings, arg) -> settings.extra(Node.op(name, priority, inner(settings, priority, arg, space)), random) + ); + } + + public void unary(final String left, final String right) { + brackets.put(left, right); + nodeRenderer.unary( + left, + (settings, arg) -> settings.extra(Node.op(left, Integer.MAX_VALUE, arg), random) + ); + } + + private Node inner(final NodeRenderer.Settings settings, final int priority, final Node arg, final String space) { + if (settings.mode() == NodeRenderer.Mode.FULL) { + return NodeRenderer.paren(true, arg); + } else { + final String op = arg.get( + c -> space, + n -> space, + (n, p, a) -> + priority > unaryPriority(arg) ? NodeRenderer.PAREN : + NodeRenderer.PAREN.equals(n) ? "" : + space, + (n, a, b) -> NodeRenderer.PAREN + ); + return op.isEmpty() ? arg : Node.op(op, Priority.MAX.priority | 1, arg); + } + } + + private static Integer unaryPriority(final Node node) { + return node.get(c -> Integer.MAX_VALUE, n -> Integer.MAX_VALUE, (n, p, a) -> p, (n, a, b) -> Integer.MIN_VALUE); + } + + public void binary(final String name, final int priority) { + final Priority mp = new Priority(name, priority); + priorities.put(name, mp); + + nodeRenderer.binary(name, (settings, l, r) -> settings.extra(process(settings, mp, l, r), random)); + } + + private Node process(final NodeRenderer.Settings settings, final Priority mp, final Node l, final Node r) { + if (settings.mode() == NodeRenderer.Mode.FULL) { + return NodeRenderer.paren(true, op(mp, l, r)); + } + + final Priority lp = priority(l); + final Priority rp = priority(r); + + final int rc = rp.compareLevels(mp); + + // :NOTE: Especially ugly code, do not replicate + final boolean advanced = settings.mode() == NodeRenderer.Mode.SAME + || mp.has(2) + || mp.has(1) && (mp != rp || (settings.mode() == NodeRenderer.Mode.TRUE_MINI && hasOther(r, rp))); + + final Node al = NodeRenderer.paren(lp.compareLevels(mp) < 0, l); + if (rc == 0 && !advanced) { + return get(r, null, (n, a, b) -> rp.op(mp.op(al, a), b)); + } else { + return mp.op(al, NodeRenderer.paren(rc == 0 && advanced || rc < 0, r)); + } + } + + private boolean hasOther(final Node node, final Priority priority) { + return get(node, () -> false, (name, l, r) -> { + final Priority p = Functional.get(priorities, name); + if (p.compareLevels(priority) != 0) { + return false; + } + return p != priority || hasOther(l, priority); + }); + } + + private Node op(final Priority mp, final Node l, final Node r) { + return mp.op(l, r); + } + + private Priority priority(final Node node) { + return get(node, () -> Priority.MAX, (n, a, b) -> Functional.get(priorities, n)); + } + + private R get(final Node node, final Supplier common, final Node.Binary, R> binary) { + return node.get( + c -> common.get(), + n -> common.get(), + (n, p, a) -> common.get(), + binary + ); + } + + public NodeRenderer build() { + return new NodeRenderer<>(nodeRenderer.build(), brackets, random); + } + + // :NOTE: Especially ugly bit-fiddling, do not replicate + private record Priority(String op, int priority) { + private static final int Q = 3; + private static final Priority MAX = new Priority("MAX", Integer.MAX_VALUE - Q); + + private int compareLevels(final Priority that) { + return (priority | Q) - (that.priority | Q); + } + + @Override + public String toString() { + return String.format("Priority(%s, %d, %d)", op, priority | Q, priority & Q); + } + + public Node op(final Node l, final Node r) { + return Node.op(op, l, r); + } + + private boolean has(final int value) { + return (priority & Q) == value; + } + } +} diff --git a/java/expression/common/Reason.java b/java/expression/common/Reason.java new file mode 100644 index 0000000..9ad5b22 --- /dev/null +++ b/java/expression/common/Reason.java @@ -0,0 +1,60 @@ +package expression.common; + +import base.Either; + +import java.util.function.LongUnaryOperator; +import java.util.function.Supplier; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Reason { + public static final Reason OVERFLOW = new Reason("Overflow"); + public static final Reason DBZ = new Reason("Division by zero"); + + private final String description; + + public Reason(final String description) { + this.description = description; + } + + public static Either eval(final Supplier action) { + try { + return Either.right(action.get()); + } catch (final ReasonException e) { + return Either.left(e.reason); + } + } + + public static int overflow(final long value) { + return value < Integer.MIN_VALUE || Integer.MAX_VALUE < value + ? OVERFLOW.error() + : (int) value; + } + + public T error() { + throw new ReasonException(this); + } + + public LongUnaryOperator less(final long limit, final LongUnaryOperator op) { + return a -> a < limit ? error() : op.applyAsLong(a); + } + + public LongUnaryOperator greater(final int limit, final LongUnaryOperator op) { + return a -> a > limit ? error() : op.applyAsLong(a); + } + + private static class ReasonException extends RuntimeException { + private final Reason reason; + + public ReasonException(final Reason reason) { + super(reason.description); + this.reason = reason; + } + } + + @Override + public String toString() { + return String.format("Reason(%s)", description); + } +} diff --git a/java/expression/common/Renderer.java b/java/expression/common/Renderer.java new file mode 100644 index 0000000..1187f31 --- /dev/null +++ b/java/expression/common/Renderer.java @@ -0,0 +1,60 @@ +package expression.common; + +import base.Functional; +import base.Pair; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Renderer { + static Builder builder(final Node.Const constant) { + return new Builder<>(constant); + } + + R render(final Expr expr, final S settings); + + @FunctionalInterface + interface UnaryOperator { + R apply(S settings, R arg); + } + + @FunctionalInterface + interface BinaryOperator { + R apply(S settings, R arg1, R arg2); + } + + final class Builder { + private final Node.Const constant; + private final Map> unary = new HashMap<>(); + private final Map> binary = new HashMap<>(); + + private Builder(final Node.Const constant) { + this.constant = constant; + } + + public void unary(final String name, final UnaryOperator op) { + unary.put(name, op); + } + + public void binary(final String name, final BinaryOperator op) { + binary.put(name, op); + } + + public Renderer build() { + return (expr, settings) -> { + final Map vars = expr.variables().stream() + .collect(Collectors.toMap(Pair::first, Pair::second)); + return expr.node().cata( + constant, + name -> Functional.get(vars, name), + (name, p, arg) -> Functional.get(unary, name).apply(settings, arg), + (name, arg1, arg2) -> Functional.get(binary, name).apply(settings, arg1, arg2) + ); + }; + } + } +} diff --git a/java/expression/common/TestGenerator.java b/java/expression/common/TestGenerator.java new file mode 100644 index 0000000..fcf7288 --- /dev/null +++ b/java/expression/common/TestGenerator.java @@ -0,0 +1,56 @@ +package expression.common; + +import base.Pair; +import base.TestCounter; +import expression.ToMiniString; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public class TestGenerator { + private final Generator generator; + private final NodeRenderer renderer; + + public TestGenerator(final Generator generator, final NodeRenderer renderer) { + this.generator = generator; + this.renderer = renderer; + } + + public void testBasic(final Consumer> test) { + generator.testBasic(consumer(test)); + } + + public void testRandom(final TestCounter counter, final int denominator, final Consumer> test) { + generator.testRandom(counter, denominator, consumer(test)); + } + + private Consumer> consumer(final Consumer> consumer) { + return expr -> consumer.accept(new TestGenerator.Test<>(expr, renderer)); + } + + + public List> variables(final int count) { + return generator.variables(count); + } + + public String render(final Expr expr, final NodeRenderer.Settings settings) { + return renderer.render(expr, settings); + } + + public static class Test { + public final Expr expr; + private final Map rendered = new HashMap<>(); + private final NodeRenderer renderer; + + public Test(final Expr expr, final NodeRenderer renderer) { + this.expr = expr; + this.renderer = renderer; + } + + public String render(final NodeRenderer.Settings settings) { + return rendered.computeIfAbsent(settings, s -> renderer.render(expr, s)); + } + } +} diff --git a/java/expression/common/TestGeneratorBuilder.java b/java/expression/common/TestGeneratorBuilder.java new file mode 100644 index 0000000..c9efb90 --- /dev/null +++ b/java/expression/common/TestGeneratorBuilder.java @@ -0,0 +1,145 @@ +package expression.common; + +import base.ExtendedRandom; +import base.Functional; +import expression.ToMiniString; +import expression.common.ExpressionKind.Variables; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class TestGeneratorBuilder { + private final ExtendedRandom random; + + private final Generator.Builder generator; + private final NodeRendererBuilder renderer; + + private final List>, Stream>>> basicTests = new ArrayList<>(); + private final List> consts; + private final boolean verbose; + + public TestGeneratorBuilder( + final ExtendedRandom random, + final Supplier constant, + final List constants, + final boolean verbose + ) { + this.random = random; + this.verbose = verbose; + + generator = Generator.builder(constant, random); + renderer = new NodeRendererBuilder<>(random); + + consts = Functional.map(constants, Node::constant); + basicTests.add(vars -> consts.stream()); + basicTests.add(List::stream); + } + + private Node c() { + return random.randomItem(consts); + } + + private Node v(final List> variables) { + return random.randomItem(variables); + } + + private static Node f(final String name, final int priority, final Node arg) { + return Node.op(name, priority, arg); + } + + private static Node f(final String left, final Node arg) { + return Node.op(left, Integer.MAX_VALUE, arg); + } + + private static Node f(final String name, final Node arg1, final Node arg2) { + return Node.op(name, arg1, arg2); + } + + @SafeVarargs + private void basicTests(final Function>, Node>... tests) { + Arrays.stream(tests).map(test -> test.andThen(Stream::of)).forEachOrdered(basicTests::add); + } + + public void unary(final String name, final int priority) { + generator.add(name, (priority & 1) * 2 - 1); + renderer.unary(name, priority); + + if (verbose) { + basicTests.add(vars -> Stream.concat(consts.stream(), vars.stream()).map(a -> f(name, priority, a))); + } else { + basicTests(vars -> f(name, priority, c()), vars -> f(name, priority, v(vars))); + } + + final Function>, Node> p1 = vars -> f(name, priority, f(name, priority, f("+", v(vars), c()))); + final Function>, Node> p2 = vars -> f("*", v(vars), f("*", v(vars), f(name, priority, c()))); + basicTests( + vars -> f(name, priority, f("+", v(vars), v(vars))), + vars -> f(name, priority, f(name, priority, v(vars))), + vars -> f(name, priority, f("/", f(name, priority, v(vars)), f("+", v(vars), v(vars)))), + p1, + p2, + vars -> f("+", p1.apply(vars), p2.apply(vars)) + ); + } + + public void unary(final String left, final String right) { + generator.add(left, 1); + renderer.unary(left, right); + + if (verbose) { + basicTests.add(vars -> Stream.concat(consts.stream(), vars.stream()).map(a -> f(left, a))); + } else { + basicTests(vars -> f(left, c()), vars -> f(left, v(vars))); + } + + final Function>, Node> p1 = vars -> f(left, f(left, f("+", v(vars), c()))); + final Function>, Node> p2 = vars -> f("*", v(vars), f("*", v(vars), f(left, c()))); + basicTests( + vars -> f(left, f("+", v(vars), v(vars))), + vars -> f(left, f(left, v(vars))), + vars -> f(left, f("/", f(left, v(vars)), f("+", v(vars), v(vars)))), + p1, + p2, + vars -> f("+", p1.apply(vars), p2.apply(vars)) + ); + } + + public void binary(final String name, final int priority) { + generator.add(name, 2); + renderer.binary(name, priority); + + if (verbose) { + basicTests.add(vars -> Stream.concat(consts.stream(), vars.stream().limit(3)) + .flatMap(a -> consts.stream().map(b -> f(name, a, b)))); + } else { + basicTests( + vars -> f(name, c(), c()), + vars -> f(name, v(vars), c()), + vars -> f(name, c(), v(vars)), + vars -> f(name, v(vars), v(vars)) + ); + } + + final Function>, Node> p1 = vars -> f(name, f(name, f("+", v(vars), c()), v(vars)), v(vars)); + final Function>, Node> p2 = vars -> f("*", v(vars), f("*", v(vars), f(name, c(), v(vars)))); + + basicTests( + vars -> f(name, f(name, v(vars), v(vars)), v(vars)), + vars -> f(name, v(vars), f(name, v(vars), v(vars))), + vars -> f(name, f(name, v(vars), v(vars)), f(name, v(vars), v(vars))), + vars -> f(name, f("-", f(name, v(vars), v(vars)), c()), f("+", v(vars), v(vars))), + p1, + p2, + vars -> f("+", p1.apply(vars), p2.apply(vars)) + ); + } + + public TestGenerator build(final Variables variables) { + return new TestGenerator<>(generator.build(variables, basicTests), renderer.build()); + } +} diff --git a/java/expression/common/Type.java b/java/expression/common/Type.java new file mode 100644 index 0000000..3b396e9 --- /dev/null +++ b/java/expression/common/Type.java @@ -0,0 +1,51 @@ +package expression.common; + +import base.Asserts; +import base.ExtendedRandom; +import expression.Const; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.function.Function; +import java.util.function.IntFunction; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Type { + private final IntFunction fromInt; + private final Function random; + private final Function constant; + + public Type(final IntFunction fromInt, final Function random, final Class type) { + this.fromInt = fromInt; + this.random = random; + + try { + final MethodHandle constructor = MethodHandles.publicLookup() + .findConstructor(Const.class, MethodType.methodType(void.class, type)); + constant = c -> { + try { + return (Const) constructor.invoke(c); + } catch (final Throwable e) { + throw Asserts.error("Cannot create new Const(%s): %s", c, e); + } + }; + } catch (final IllegalAccessException | NoSuchMethodException e) { + throw Asserts.error("Cannot find constructor Const(%s): %s", type, e); + } + } + + public Const constant(final C value) { + return constant.apply(value); + } + + public C fromInt(final int value) { + return fromInt.apply(value); + } + + public C randomValue(final ExtendedRandom random) { + return this.random.apply(random); + } +} diff --git a/java/expression/common/package-info.java b/java/expression/common/package-info.java new file mode 100644 index 0000000..d595890 --- /dev/null +++ b/java/expression/common/package-info.java @@ -0,0 +1,7 @@ +/** + * Expressions generators for expression-based homeworks + * of Paradigms of Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression.common; \ No newline at end of file diff --git a/java/expression/exceptions/CheckedAdd.java b/java/expression/exceptions/CheckedAdd.java new file mode 100644 index 0000000..88d38fe --- /dev/null +++ b/java/expression/exceptions/CheckedAdd.java @@ -0,0 +1,29 @@ +package expression.exceptions; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import expression.*; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class CheckedAdd extends AbstractBinaryOperation { + public CheckedAdd(AbstractExpression l, AbstractExpression r) { super(l, r); } + + @Override protected String getOperator() { return "+"; } + @Override protected int getPriority() { return 1; } + @Override protected boolean isRightAssoc() { return false; } + + @Override + protected int applyInt(int a, int b) { + int result = a + b; + if (((a ^ result) & (b ^ result)) < 0) { + throw new OverflowException("addition"); + } + return result; + } + + @Override protected BigInteger applyBi(BigInteger a, BigInteger b) { return a.add(b); } + @Override protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { return a.add(b); } +} \ No newline at end of file diff --git a/java/expression/exceptions/CheckedDivide.java b/java/expression/exceptions/CheckedDivide.java new file mode 100644 index 0000000..0713133 --- /dev/null +++ b/java/expression/exceptions/CheckedDivide.java @@ -0,0 +1,37 @@ +package expression.exceptions; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; + +import expression.*; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class CheckedDivide extends AbstractBinaryOperation { + public CheckedDivide(AbstractExpression l, AbstractExpression r) { super(l, r); } + + @Override protected String getOperator() { return "/"; } + @Override protected int getPriority() { return 2; } + @Override protected boolean isRightAssoc() { return true; } + + @Override + protected int applyInt(int a, int b) { + if (b == 0) throw new DivisionByZeroException(); + if (a == Integer.MIN_VALUE && b == -1) throw new OverflowException("division"); + return a / b; + } + + @Override + protected BigInteger applyBi(BigInteger a, BigInteger b) { + if (b.signum() == 0) throw new DivisionByZeroException(); + return a.divide(b); + } + + @Override + protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { + if (b.signum() == 0) throw new DivisionByZeroException(); + return a.divide(b, MathContext.DECIMAL128); + } +} \ No newline at end of file diff --git a/java/expression/exceptions/CheckedMultiply.java b/java/expression/exceptions/CheckedMultiply.java new file mode 100644 index 0000000..91a1398 --- /dev/null +++ b/java/expression/exceptions/CheckedMultiply.java @@ -0,0 +1,32 @@ +package expression.exceptions; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import expression.*; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class CheckedMultiply extends AbstractBinaryOperation { + public CheckedMultiply(AbstractExpression l, AbstractExpression r) { super(l, r); } + + @Override protected String getOperator() { return "*"; } + @Override protected int getPriority() { return 2; } + @Override protected boolean isRightAssoc() { return false; } + + @Override + protected int applyInt(int a, int b) { + if (a == 0 || b == 0) return 0; + if (a == Integer.MIN_VALUE && b == -1) throw new OverflowException("multiplication"); + if (b == Integer.MIN_VALUE && a == -1) throw new OverflowException("multiplication"); + int result = a * b; + if (result / a != b) { + throw new OverflowException("multiplication"); + } + return result; + } + + @Override protected BigInteger applyBi(BigInteger a, BigInteger b) { return a.multiply(b); } + @Override protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { return a.multiply(b); } +} \ No newline at end of file diff --git a/java/expression/exceptions/CheckedNegate.java b/java/expression/exceptions/CheckedNegate.java new file mode 100644 index 0000000..fbcb96f --- /dev/null +++ b/java/expression/exceptions/CheckedNegate.java @@ -0,0 +1,44 @@ +package expression.exceptions; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +import expression.*; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class CheckedNegate extends AbstractExpression { + private final AbstractExpression operand; + + public CheckedNegate(AbstractExpression operand) { + this.operand = operand; + } + + private static int checkedNegate(int n) { + if (n == Integer.MIN_VALUE) throw new OverflowException("negation"); + return -n; + } + + @Override public int evaluate(int x) { return checkedNegate(operand.evaluate(x)); } + @Override public int evaluate(int x, int y, int z) { return checkedNegate(operand.evaluate(x,y,z)); } + @Override public int evaluate(List vars) { return checkedNegate(operand.evaluate(vars)); } + @Override public BigInteger evaluateBi(List vars) { return operand.evaluateBi(vars).negate(); } + @Override public BigDecimal evaluateBd(List vars) { return operand.evaluateBd(vars).negate(); } + + @Override public String toString() { return "-(" + operand + ")"; } + @Override public String toMiniString() { + if (operand instanceof AbstractBinaryOperation) return "-(" + operand.toMiniString() + ")"; + return "- " + operand.toMiniString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof CheckedNegate)) return false; + return operand.equals(((CheckedNegate) obj).operand); + } + + @Override public int hashCode() { return -operand.hashCode(); } +} \ No newline at end of file diff --git a/java/expression/exceptions/CheckedSubtract.java b/java/expression/exceptions/CheckedSubtract.java new file mode 100644 index 0000000..85a40d4 --- /dev/null +++ b/java/expression/exceptions/CheckedSubtract.java @@ -0,0 +1,31 @@ +package expression.exceptions; + +import expression.AbstractBinaryOperation; +import expression.AbstractExpression; +import expression.OverflowException; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class CheckedSubtract extends AbstractBinaryOperation { + public CheckedSubtract(AbstractExpression l, AbstractExpression r) { super(l, r); } + + @Override protected String getOperator() { return "-"; } + @Override protected int getPriority() { return 1; } + @Override protected boolean isRightAssoc() { return true; } + + @Override + protected int applyInt(int a, int b) { + int result = a - b; + if (((a ^ b) & (a ^ result)) < 0) { + throw new OverflowException("subtraction"); + } + return result; + } + + @Override protected BigInteger applyBi(BigInteger a, BigInteger b) { return a.subtract(b); } + @Override protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { return a.subtract(b); } +} \ No newline at end of file diff --git a/java/expression/exceptions/ExceptionsTest.java b/java/expression/exceptions/ExceptionsTest.java new file mode 100644 index 0000000..2984a68 --- /dev/null +++ b/java/expression/exceptions/ExceptionsTest.java @@ -0,0 +1,30 @@ +package expression.exceptions; + +import base.Selector; +import expression.ListExpression; +import expression.parser.Operations; + +import static expression.parser.Operations.*; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ExceptionsTest { + private static final ExpressionParser PARSER = new ExpressionParser(); + private static final Operations.Operation LIST = kind(ListExpression.KIND, PARSER::parse); + + public static final Selector SELECTOR = Selector.composite(ExceptionsTest.class, ExceptionsTester::new, "easy", "hard") + .variant("Base", LIST, ADD, SUBTRACT, MULTIPLY, DIVIDE, NEGATE) + .variant("3637", ABS, CUBE, CBRT) + .variant("3839", ABS, CUBE, CBRT, SQUARE, SQRT) + .variant("3435", ABS, SQRT) + .variant("3233", ABS, CBRT) + .selector(); + + private ExceptionsTest() { + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/expression/exceptions/ExceptionsTestSet.java b/java/expression/exceptions/ExceptionsTestSet.java new file mode 100644 index 0000000..43728ac --- /dev/null +++ b/java/expression/exceptions/ExceptionsTestSet.java @@ -0,0 +1,162 @@ +package expression.exceptions; + +import base.Functional; +import base.Named; +import base.Pair; +import expression.ToMiniString; +import expression.Variable; +import expression.common.ExpressionKind; +import expression.parser.ParserTestSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.LongBinaryOperator; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ExceptionsTestSet extends ParserTestSet { + private static final int D = 5; + private static final List OVERFLOW_VALUES = new ArrayList<>(); + private final char[] CHARS = "AZ+-*%()[]<>".toCharArray(); + + static { + Functional.addRange(OVERFLOW_VALUES, D, Integer.MIN_VALUE + D); + Functional.addRange(OVERFLOW_VALUES, D, Integer.MIN_VALUE / 2); + Functional.addRange(OVERFLOW_VALUES, D, (int) -Math.sqrt(Integer.MAX_VALUE)); + Functional.addRange(OVERFLOW_VALUES, D, 0); + Functional.addRange(OVERFLOW_VALUES, D, (int) Math.sqrt(Integer.MAX_VALUE)); + Functional.addRange(OVERFLOW_VALUES, D, Integer.MAX_VALUE / 2); + Functional.addRange(OVERFLOW_VALUES, D, Integer.MAX_VALUE - D); + } + + private final List> parsingTest; + + public ExceptionsTestSet(final ExceptionsTester tester, final ParsedKind kind) { + super(tester, kind, false); + parsingTest = tester.parsingTest; + } + + private void testParsingErrors() { + counter.testForEach(parsingTest, op -> { + final List names = Functional.map(kind.kind().variables().generate(counter.random(), 3), Pair::first); + final String expr = mangle(op.value(), names); + try { + kind.parse(expr, names); + counter.fail("Successfully parsed '%s'", op.value()); + } catch (final Exception e) { + counter.format("%-30s %s%n", op.name(), e.getClass().getSimpleName() + ": " + e.getMessage()); + } + }); + } + + private void testOverflow() { + final List> variables = kind.kind().variables().generate(counter.random(), 3); + final List names = Functional.map(variables, Pair::first); + final Variable vx = (Variable) variables.get(0).second(); + final Variable vy = (Variable) variables.get(1).second(); + + //noinspection Convert2MethodRef + testOverflow(names, (a, b) -> a + b, "+", new CheckedAdd(vx, vy)); + testOverflow(names, (a, b) -> a - b, "-", new CheckedSubtract(vx, vy)); + testOverflow(names, (a, b) -> a * b, "*", new CheckedMultiply(vx, vy)); + testOverflow(names, (a, b) -> b == 0 ? Long.MAX_VALUE : a / b, "/", new CheckedDivide(vx, vy)); + testOverflow(names, (a, b) -> -b, "<- ignore first argument, unary -", new CheckedNegate(vy)); + } + + private void testOverflow(final List names, final LongBinaryOperator f, final String op, final Object expression) { + final ExpressionKind kind = this.kind.kind(); + for (final int a : OVERFLOW_VALUES) { + for (final int b : OVERFLOW_VALUES) { + final long expected = f.applyAsLong(a, b); + final boolean isInt = Integer.MIN_VALUE <= expected && expected <= Integer.MAX_VALUE; + try { + final C actual = kind.evaluate( + kind.cast(expression), + names, + kind.fromInts(List.of(a, b, 0)) + ); + counter.checkTrue( + isInt && kind.fromInt((int) expected).equals(actual), + "%d %s %d == %d", a, op, b, actual + ); + } catch (final Exception e) { + if (isInt) { + counter.fail(e, "Unexpected error in %d %s %d", a, op, b); + } + } + } + } + } + + @Override + protected void test() { + counter.scope("Overflow tests", (Runnable) this::testOverflow); + super.test(); + counter.scope("Parsing error tests", this::testParsingErrors); + } + + + @Override + protected E parse(final String expression, final List variables, final boolean reparse) { + final String expr = expression.strip(); + if (expr.length() > 10) { + for (final char ch : CHARS) { + for (int i = 0; i < 10; i++) { + final int index = 1 + tester.random().nextInt(expr.length() - 2); + int pi = index - 1; + while (Character.isWhitespace(expr.charAt(pi))) { + pi--; + } + int ni = index; + while (Character.isWhitespace(expr.charAt(ni))) { + ni++; + } + final char pc = expr.charAt(pi); + final char nc = expr.charAt(ni); + if ( + "-([{*∛√²³₂₃!‖⎵⎴⌊⌈=?".indexOf(nc) < 0 && + (!Character.isLetterOrDigit(pc) || !Character.isLetterOrDigit(ch)) && + nc != ch && pc != ch && + !Character.isLetterOrDigit(nc) && nc != '$' + ) { + shouldFail( + variables, + "Parsing error expected for " + insert(expr, index, "" + ch + "<-- ERROR_INSERTED>"), + insert(expr, index, String.valueOf(ch)) + ); + break; + } + } + } + parens(variables, expr, '[', ']'); + parens(variables, expr, '{', '}'); + } + + return counter.testV(() -> counter.call("parse", () -> kind.parse(expr, variables))); + } + + private static String insert(final String expr, final int index, final String value) { + return expr.substring(0, index) + value + expr.substring(index); + } + + private void parens(final List variables, final String expr, final char open, final char close) { + if (expr.indexOf(open) >= 0) { + replaces(variables, expr, open, '('); + replaces(variables, expr, close, ')'); + if (expr.indexOf('(') >= 0) { + replaces(variables, expr, '(', open); + replaces(variables, expr, ')', close); + } + } + } + + private void replaces(final List variables, final String expr, final char what, final char by) { + final String input = expr.replace(what, by); + shouldFail(variables, "Unmatched parentheses: " + input, input); + } + + private void shouldFail(final List variables, final String message, final String input) { + counter.shouldFail(message, () -> kind.parse(input, variables)); + } +} diff --git a/java/expression/exceptions/ExceptionsTester.java b/java/expression/exceptions/ExceptionsTester.java new file mode 100644 index 0000000..df9048b --- /dev/null +++ b/java/expression/exceptions/ExceptionsTester.java @@ -0,0 +1,100 @@ +package expression.exceptions; + +import base.Named; +import base.TestCounter; +import expression.common.Reason; +import expression.parser.ParserTestSet; +import expression.parser.ParserTester; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.LongBinaryOperator; +import java.util.function.LongToIntFunction; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ExceptionsTester extends ParserTester { + /* package-private */ final List> parsingTest = new ArrayList<>(List.of( + Named.of("No first argument", "* $y * $z"), + Named.of("No middle argument", "$x * * $z"), + Named.of("No last argument", "$x * $y * "), + Named.of("No first argument'", "1 + (* $y * $z) + 2"), + Named.of("No middle argument'", "1 + ($x * / 9) + 3"), + Named.of("No last argument'", "1 + ($x * $y - ) + 3"), + Named.of("No opening parenthesis", "$x * $y)"), + Named.of("No closing parenthesis", "($x * $y"), + Named.of("Mismatched closing parenthesis", "($x * $y]"), + Named.of("Mismatched open parenthesis", "[$x * $y)"), + Named.of("Start symbol", "@$x * $y"), + Named.of("Middle symbol", "$x @ * $y"), + Named.of("End symbol", "$x * $y@"), + Named.of("Constant overflow 1", Integer.MIN_VALUE - 1L + ""), + Named.of("Constant overflow 2", Integer.MAX_VALUE + 1L + ""), + Named.of("Bare +", "+"), + Named.of("Bare -", "-"), + Named.of("Bare a", "a"), + Named.of("(())", "(())"), + Named.of("Spaces in numbers", "10 20") + )); + + public ExceptionsTester(final TestCounter counter) { + super(counter); + } + + + private void parsingTests(final String... tests) { + for (final String test : tests) { + parsingTest.add(Named.of(test, test)); + } + } + + @Override + public void unary(final String name, final int priority, final BiFunction op) { + if (allowed(name)) { + parsingTests(name, "1 * " + name, name + " * 1"); + } + parsingTests(name + "()", name + "(1, 2)"); + if (name.length() > 1) { + parsingTests(name + "q"); + } + if (allLetterAndDigit(name)) { + parsingTests(name + "1", name + "q"); + } + super.unary(name, priority, op); + } + + private static boolean allowed(final String name) { + return !"xyz".contains(name.substring(0, 1)) && !"xyz".contains(name.substring(name.length() - 1)); + } + + @Override + public void binary(final String name, final int priority, final LongBinaryOperator op) { + if (allowed(name)) { + parsingTests(name); + } + parsingTests("1 " + name, "1 " + name + " * 3"); + if (!"-".equals(name)) { + parsingTests(name + " 1", "1 * " + name + " 2"); + } + if (allLetterAndDigit(name)) { + parsingTests("5" + name + "5", "5 " + name + "5", "5 " + name + "5 5", "1" + name + "x 1", "1 " + name + "x 1"); + } + super.binary(name, priority, op); + } + + private static boolean allLetterAndDigit(final String name) { + return name.chars().allMatch(Character::isLetterOrDigit); + } + + @Override + protected void test(final ParserTestSet.ParsedKind kind) { + new ExceptionsTestSet<>(this, kind).test(); + } + + @Override + protected int cast(final long value) { + return Reason.overflow(value); + } +} diff --git a/java/expression/exceptions/ExpressionParser.java b/java/expression/exceptions/ExpressionParser.java new file mode 100644 index 0000000..e285a43 --- /dev/null +++ b/java/expression/exceptions/ExpressionParser.java @@ -0,0 +1,215 @@ +package expression.exceptions; + +import expression.*; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class ExpressionParser implements ListParser { + + private String src; + private int pos; + private java.util.List variables; + + @Override + public ListExpression parse(String expression, java.util.List variables) { + // System.err.println("PARSE: [" + expression + "]"); + this.src = expression; + this.pos = 0; + this.variables = variables; + AbstractExpression result = parseMinMax(); + skipWhitespace(); + if (pos < src.length()) { + throw new IllegalArgumentException( + "Unexpected character '" + src.charAt(pos) + "' at position " + pos); + } + return result; + } + + private AbstractExpression parseMinMax() { + AbstractExpression left = parseAddSub(); + while (true) { + skipWhitespace(); + if (tryConsume("min")) left = new Min(left, parseAddSub()); + else if (tryConsume("max")) left = new Max(left, parseAddSub()); + else if (tryConsume("set")) left = new SetBit(left, parseAddSub()); + else if (tryConsume("clear")) left = new Clear(left, parseAddSub()); + else break; + } + return left; + } + + private AbstractExpression parseAddSub() { + AbstractExpression left = parseMulDiv(); + while (true) { + skipWhitespace(); + if (pos < src.length() && src.charAt(pos) == '+') { + pos++; + left = new CheckedAdd(left, parseMulDiv()); + } else if (pos < src.length() && src.charAt(pos) == '-') { + pos++; + left = new CheckedSubtract(left, parseMulDiv()); + } else break; + } + return left; + } + + private AbstractExpression parseMulDiv() { + AbstractExpression left = parsePower(); + while (true) { + skipWhitespace(); + if (pos < src.length() && src.charAt(pos) == '*' && nextCharIs('*')) { + pos++; + left = new CheckedMultiply(left, parsePower()); + } else if (pos < src.length() && src.charAt(pos) == '/' && nextCharIs('/')) { + pos++; + left = new CheckedDivide(left, parsePower()); + } else break; + } + return left; + } + + private AbstractExpression parsePower() { + AbstractExpression left = parseUnary(); + while (true) { + skipWhitespace(); + if (pos + 1 < src.length() && src.charAt(pos) == '*' && src.charAt(pos + 1) == '*') { + pos += 2; + left = new Power(left, parseUnary()); + } else if (pos + 1 < src.length() && src.charAt(pos) == '/' && src.charAt(pos + 1) == '/') { + pos += 2; + left = new Log(left, parseUnary()); + } else break; + } + return left; + } + + private AbstractExpression parseUnary() { + skipWhitespace(); + if (pos >= src.length()) + throw new IllegalArgumentException("Unexpected end of expression at position " + pos); + + if (src.charAt(pos) == '-') { + pos++; + if (pos < src.length() && Character.isDigit(src.charAt(pos))) return parsePostfix(parseNumber(true)); + return new CheckedNegate(parseUnary()); + } + + // abs: ||x|| (U+2016 DOUBLE VERTICAL LINE) + if (src.charAt(pos) == '‖') { + pos++; + AbstractExpression inner = parseMinMax(); + skipWhitespace(); + if (pos >= src.length() || src.charAt(pos) != '‖') + throw new IllegalArgumentException("Expected ‖ at position " + pos); + pos++; + return parsePostfix(new Abs(inner)); + } + + // prefix unary: sqrt U+221A, cbrt U+221B + if (src.charAt(pos) == '√') { pos++; return parsePostfix(new Sqrt(parseUnary())); } + if (src.charAt(pos) == '∛') { pos++; return parsePostfix(new Cbrt(parseUnary())); } + + if (tryConsume("reverse")) return parsePostfix(new Reverse(parseUnary())); + if (tryConsume("digits")) return parsePostfix(new Digits(parseUnary())); + if (tryConsume("floor")) return parsePostfix(new Floor(parseUnary())); + if (tryConsume("ceiling")) return parsePostfix(new Ceiling(parseUnary())); + if (tryConsume("log₂") || tryConsume("log2")) return parsePostfix(new Log2(parseUnary())); + if (tryConsume("pow₂") || tryConsume("pow2")) return parsePostfix(new Pow2(parseUnary())); + if (tryConsume("low")) return parsePostfix(new Low(parseUnary())); + if (tryConsume("high")) return parsePostfix(new High(parseUnary())); + + return parsePostfix(parsePrimary()); + } + + private AbstractExpression parsePostfix(AbstractExpression base) { + skipWhitespace(); + if (pos < src.length() && src.charAt(pos) == '²') { pos++; return parsePostfix(new Square(base)); } + if (pos < src.length() && src.charAt(pos) == '³') { pos++; return parsePostfix(new Cube(base)); } + return base; + } + + private AbstractExpression parsePrimary() { + skipWhitespace(); + if (pos >= src.length()) throw new IllegalArgumentException("Unexpected end of expression"); + char c = src.charAt(pos); + + if (c == '(') { + pos++; + AbstractExpression inner = parseMinMax(); + skipWhitespace(); + expect(); + return inner; + } + if (c == '$') { pos++; return new Variable(parseIndex()); } + if (Character.isDigit(c)) return parseNumber(false); + + if (Character.isLetter(c)) { + int start = pos; + while (pos < src.length() && (Character.isLetterOrDigit(src.charAt(pos)) || src.charAt(pos) == '_')) + pos++; + String name = src.substring(start, pos); + int idx = variables != null ? variables.indexOf(name) : -1; + if (idx >= 0) return new Variable(idx, name); + throw new IllegalArgumentException("Unknown identifier '" + name + "' at position " + start); + } + + throw new IllegalArgumentException("Unexpected character '" + c + "' at position " + pos); + } + + private void skipWhitespace() { + while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) pos++; + } + + private boolean nextCharIs(char next) { + return pos + 1 >= src.length() || src.charAt(pos + 1) != next; + } + + private boolean tryConsume(String keyword) { + skipWhitespace(); + if (!src.startsWith(keyword, pos)) return false; + int end = pos + keyword.length(); + if (end < src.length()) { + char next = src.charAt(end); + if (Character.isLetterOrDigit(next) || next == '_') return false; + } + if (variables != null && variables.contains(keyword)) return false; + pos = end; + return true; + } + + private void expect() { + if (pos >= src.length() || src.charAt(pos) != ')') + throw new IllegalArgumentException("Expected '" + ')' + "' at position " + pos + + (pos < src.length() ? ", got '" + src.charAt(pos) + "'" : ", got end of input")); + pos++; + } + + private AbstractExpression parseNumber(boolean negative) { + int start = pos; + while (pos < src.length() && Character.isDigit(src.charAt(pos))) pos++; + if (start == pos) throw new IllegalArgumentException("Expected digit at position " + pos); + String numStr = src.substring(start, pos); + int result = 0; + for (int i = 0; i < numStr.length(); i++) { + int digit = numStr.charAt(i) - '0'; + if (!negative) { + if (result > (Integer.MAX_VALUE - digit) / 10) + throw new OverflowException("constant " + numStr); + result = result * 10 + digit; + } else { + if (result < (Integer.MIN_VALUE + digit) / 10) + throw new OverflowException("constant -" + numStr); + result = result * 10 - digit; + } + } + return new Const(result); + } + + private int parseIndex() { + int start = pos; + while (pos < src.length() && Character.isDigit(src.charAt(pos))) pos++; + if (start == pos) throw new IllegalArgumentException("Expected digit after '$' at position " + pos); + return Integer.parseInt(src.substring(start, pos)); + } +} \ No newline at end of file diff --git a/java/expression/exceptions/ListParser.java b/java/expression/exceptions/ListParser.java new file mode 100644 index 0000000..2370790 --- /dev/null +++ b/java/expression/exceptions/ListParser.java @@ -0,0 +1,13 @@ +package expression.exceptions; + +import expression.ListExpression; + +import java.util.List; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +public interface ListParser { + ListExpression parse(String expression, final List variables) throws Exception; +} diff --git a/java/expression/exceptions/package-info.java b/java/expression/exceptions/package-info.java new file mode 100644 index 0000000..73cbb59 --- /dev/null +++ b/java/expression/exceptions/package-info.java @@ -0,0 +1,8 @@ +/** + * Tests for Expression Error Handling homework + * of Paradigms of Programming course. + * + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression.exceptions; \ No newline at end of file diff --git a/java/expression/generic/ArithmeticType.java b/java/expression/generic/ArithmeticType.java new file mode 100644 index 0000000..28b80f3 --- /dev/null +++ b/java/expression/generic/ArithmeticType.java @@ -0,0 +1,25 @@ +package expression.generic; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public interface ArithmeticType { + T fromInt(int value); + T add(T a, T b); + T subtract(T a, T b); + T multiply(T a, T b); + T divide(T a, T b); + T negate(T a); + + default T count(T a) { + throw new UnsupportedOperationException("count"); + } + + default T min(T a, T b) { + throw new UnsupportedOperationException("min"); + } + + default T max(T a, T b) { + throw new UnsupportedOperationException("max"); + } +} diff --git a/java/expression/generic/BigIntegerType.java b/java/expression/generic/BigIntegerType.java new file mode 100644 index 0000000..1695ca3 --- /dev/null +++ b/java/expression/generic/BigIntegerType.java @@ -0,0 +1,61 @@ +package expression.generic; + +import java.math.BigInteger; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class BigIntegerType implements ArithmeticType { + + public static final BigIntegerType INSTANCE = new BigIntegerType(); + + private BigIntegerType() {} + + @Override + public BigInteger fromInt(int value) { + return BigInteger.valueOf(value); + } + + @Override + public BigInteger add(BigInteger a, BigInteger b) { + return a.add(b); + } + + @Override + public BigInteger subtract(BigInteger a, BigInteger b) { + return a.subtract(b); + } + + @Override + public BigInteger multiply(BigInteger a, BigInteger b) { + return a.multiply(b); + } + + @Override + public BigInteger divide(BigInteger a, BigInteger b) { + if (b.equals(BigInteger.ZERO)) throw new ArithmeticException( + "Division by zero" + ); + return a.divide(b); + } + + @Override + public BigInteger negate(BigInteger a) { + return a.negate(); + } + + @Override + public BigInteger count(BigInteger a) { + return BigInteger.valueOf(a.bitCount()); + } + + @Override + public BigInteger min(BigInteger a, BigInteger b) { + return a.min(b); + } + + @Override + public BigInteger max(BigInteger a, BigInteger b) { + return a.max(b); + } +} diff --git a/java/expression/generic/DoubleType.java b/java/expression/generic/DoubleType.java new file mode 100644 index 0000000..a7e4ed8 --- /dev/null +++ b/java/expression/generic/DoubleType.java @@ -0,0 +1,56 @@ +package expression.generic; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class DoubleType implements ArithmeticType { + + public static final DoubleType INSTANCE = new DoubleType(); + + private DoubleType() {} + + @Override + public Double fromInt(int value) { + return (double) value; + } + + @Override + public Double add(Double a, Double b) { + return a + b; + } + + @Override + public Double subtract(Double a, Double b) { + return a - b; + } + + @Override + public Double multiply(Double a, Double b) { + return a * b; + } + + @Override + public Double divide(Double a, Double b) { + return a / b; + } + + @Override + public Double negate(Double a) { + return -a; + } + + @Override + public Double count(Double a) { + return (double) Long.bitCount(Double.doubleToLongBits(a)); + } + + @Override + public Double min(Double a, Double b) { + return Math.min(a, b); + } + + @Override + public Double max(Double a, Double b) { + return Math.max(a, b); + } +} diff --git a/java/expression/generic/FloatType.java b/java/expression/generic/FloatType.java new file mode 100644 index 0000000..faac0e9 --- /dev/null +++ b/java/expression/generic/FloatType.java @@ -0,0 +1,56 @@ +package expression.generic; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class FloatType implements ArithmeticType { + + public static final FloatType INSTANCE = new FloatType(); + + private FloatType() {} + + @Override + public Float fromInt(int value) { + return (float) value; + } + + @Override + public Float add(Float a, Float b) { + return a + b; + } + + @Override + public Float subtract(Float a, Float b) { + return a - b; + } + + @Override + public Float multiply(Float a, Float b) { + return a * b; + } + + @Override + public Float divide(Float a, Float b) { + return a / b; + } + + @Override + public Float negate(Float a) { + return -a; + } + + @Override + public Float count(Float a) { + return (float) Integer.bitCount(Float.floatToIntBits(a)); + } + + @Override + public Float min(Float a, Float b) { + return Math.min(a, b); + } + + @Override + public Float max(Float a, Float b) { + return Math.max(a, b); + } +} diff --git a/java/expression/generic/GenericExpr.java b/java/expression/generic/GenericExpr.java new file mode 100644 index 0000000..281c7ec --- /dev/null +++ b/java/expression/generic/GenericExpr.java @@ -0,0 +1,10 @@ +package expression.generic; + +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public interface GenericExpr { + T evaluate(ArithmeticType type, List vars); +} diff --git a/java/expression/generic/GenericParser.java b/java/expression/generic/GenericParser.java new file mode 100644 index 0000000..d38bc7e --- /dev/null +++ b/java/expression/generic/GenericParser.java @@ -0,0 +1,239 @@ +package expression.generic; + +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class GenericParser { + + private static final List VARIABLES = List.of("x", "y", "z"); + + private String src; + private int pos; + + public GenericExpr parse(String expression) { + this.src = expression; + this.pos = 0; + GenericExpr result = parseMinMax(); + skipWhitespace(); + if (pos < src.length()) { + throw new IllegalArgumentException( + "Unexpected character '" + + src.charAt(pos) + + "' at position " + + pos + ); + } + return result; + } + + private GenericExpr parseMinMax() { + GenericExpr left = parseAddSub(); + while (true) { + skipWhitespace(); + if (matchKeyword("min")) { + GenericExpr right = parseAddSub(); + GenericExpr l = left; + left = (type, vars) -> + type.min( + l.evaluate(type, vars), + right.evaluate(type, vars) + ); + } else if (matchKeyword("max")) { + GenericExpr right = parseAddSub(); + GenericExpr l = left; + left = (type, vars) -> + type.max( + l.evaluate(type, vars), + right.evaluate(type, vars) + ); + } else { + break; + } + } + return left; + } + + private GenericExpr parseAddSub() { + GenericExpr left = parseMulDiv(); + while (true) { + skipWhitespace(); + if (pos < src.length() && src.charAt(pos) == '+') { + pos++; + GenericExpr right = parseMulDiv(); + GenericExpr l = left; + left = (type, vars) -> + type.add( + l.evaluate(type, vars), + right.evaluate(type, vars) + ); + } else if ( + pos < src.length() && src.charAt(pos) == '-' && !nextIsKeyword() + ) { + pos++; + GenericExpr right = parseMulDiv(); + GenericExpr l = left; + left = (type, vars) -> + type.subtract( + l.evaluate(type, vars), + right.evaluate(type, vars) + ); + } else { + break; + } + } + return left; + } + + private GenericExpr parseMulDiv() { + GenericExpr left = parseUnary(); + while (true) { + skipWhitespace(); + if (pos < src.length() && src.charAt(pos) == '*') { + pos++; + GenericExpr right = parseUnary(); + GenericExpr l = left; + left = (type, vars) -> + type.multiply( + l.evaluate(type, vars), + right.evaluate(type, vars) + ); + } else if (pos < src.length() && src.charAt(pos) == '/') { + pos++; + GenericExpr right = parseUnary(); + GenericExpr l = left; + left = (type, vars) -> + type.divide( + l.evaluate(type, vars), + right.evaluate(type, vars) + ); + } else { + break; + } + } + return left; + } + + private GenericExpr parseUnary() { + skipWhitespace(); + if (pos >= src.length()) { + throw new IllegalArgumentException( + "Unexpected end of expression at position " + pos + ); + } + if (src.charAt(pos) == '-') { + pos++; + skipWhitespace(); + if (pos < src.length() && Character.isDigit(src.charAt(pos))) { + return parseNumber(true); + } + GenericExpr operand = parseUnary(); + return (type, vars) -> type.negate(operand.evaluate(type, vars)); + } + if (matchKeyword("count")) { + GenericExpr operand = parseUnary(); + return (type, vars) -> type.count(operand.evaluate(type, vars)); + } + return parsePrimary(); + } + + private GenericExpr parsePrimary() { + skipWhitespace(); + if (pos >= src.length()) { + throw new IllegalArgumentException("Unexpected end of expression"); + } + char c = src.charAt(pos); + + if (c == '(') { + pos++; + GenericExpr inner = parseMinMax(); + skipWhitespace(); + expect(); + return inner; + } + + if (Character.isDigit(c)) { + return parseNumber(false); + } + + if (Character.isLetter(c)) { + int start = pos; + while ( + pos < src.length() && Character.isLetterOrDigit(src.charAt(pos)) + ) { + pos++; + } + String name = src.substring(start, pos); + int idx = VARIABLES.indexOf(name); + if (idx >= 0) { + return (type, vars) -> vars.get(idx); + } + throw new IllegalArgumentException( + "Unknown variable '" + name + "' at position " + start + ); + } + + throw new IllegalArgumentException( + "Unexpected character '" + c + "' at position " + pos + ); + } + + private GenericExpr parseNumber(boolean negative) { + int start = pos; + while (pos < src.length() && Character.isDigit(src.charAt(pos))) { + pos++; + } + if (start == pos) { + throw new IllegalArgumentException( + "Expected digit at position " + pos + ); + } + String numStr = src.substring(start, pos); + long val = Long.parseLong(numStr); + if (negative) val = -val; + if (val < Integer.MIN_VALUE || val > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Integer overflow: " + val); + } + int intVal = (int) val; + return (type, vars) -> type.fromInt(intVal); + } + + private boolean matchKeyword(String keyword) { + int i = pos; + for (char ch : keyword.toCharArray()) { + if (i >= src.length() || src.charAt(i) != ch) return false; + i++; + } + if ( + i < src.length() && Character.isLetterOrDigit(src.charAt(i)) + ) return false; + pos = i; + return true; + } + + private boolean nextIsKeyword() { + return false; + } + + private void skipWhitespace() { + while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) { + pos++; + } + } + + private void expect() { + if (pos >= src.length() || src.charAt(pos) != ')') { + throw new IllegalArgumentException( + "Expected '" + + ')' + + "' at position " + + pos + + (pos < src.length() + ? ", got '" + src.charAt(pos) + "'" + : ", got end of input") + ); + } + pos++; + } +} diff --git a/java/expression/generic/GenericTabulator.java b/java/expression/generic/GenericTabulator.java new file mode 100644 index 0000000..95694d5 --- /dev/null +++ b/java/expression/generic/GenericTabulator.java @@ -0,0 +1,135 @@ +package expression.generic; + +import java.math.BigInteger; +import java.util.List; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class GenericTabulator implements Tabulator { + + @Override + public Object[][][] tabulate( + String mode, + String expression, + int x1, + int x2, + int y1, + int y2, + int z1, + int z2 + ) throws Exception { + return switch (mode) { + case "i" -> compute( + IntCheckedType.INSTANCE, + expression, + x1, + x2, + y1, + y2, + z1, + z2 + ); + case "d" -> compute( + DoubleType.INSTANCE, + expression, + x1, + x2, + y1, + y2, + z1, + z2 + ); + case "bi" -> compute( + BigIntegerType.INSTANCE, + expression, + x1, + x2, + y1, + y2, + z1, + z2 + ); + case "u" -> compute( + IntUncheckedType.INSTANCE, + expression, + x1, + x2, + y1, + y2, + z1, + z2 + ); + case "s" -> compute( + ShortType.INSTANCE, + expression, + x1, + x2, + y1, + y2, + z1, + z2 + ); + case "f" -> compute( + FloatType.INSTANCE, + expression, + x1, + x2, + y1, + y2, + z1, + z2 + ); + case "t", "it" -> compute( + IntTruncType.INSTANCE, + expression, + x1, + x2, + y1, + y2, + z1, + z2 + ); + default -> throw new IllegalArgumentException( + "Unknown mode: " + mode + ); + }; + } + + private Object[][][] compute( + ArithmeticType type, + String expression, + int x1, + int x2, + int y1, + int y2, + int z1, + int z2 + ) { + GenericExpr expr = new GenericParser().parse(expression); + + int xLen = x2 - x1 + 1; + int yLen = y2 - y1 + 1; + int zLen = z2 - z1 + 1; + Object[][][] result = new Object[xLen][yLen][zLen]; + + for (int xi = 0; xi < xLen; xi++) { + for (int yi = 0; yi < yLen; yi++) { + for (int zi = 0; zi < zLen; zi++) { + T x = type.fromInt(x1 + xi); + T y = type.fromInt(y1 + yi); + T z = type.fromInt(z1 + zi); + try { + result[xi][yi][zi] = expr.evaluate( + type, + List.of(x, y, z) + ); + } catch (Exception e) { + result[xi][yi][zi] = null; + } + } + } + } + return result; + } +} diff --git a/java/expression/generic/GenericTest.java b/java/expression/generic/GenericTest.java new file mode 100644 index 0000000..e5bb980 --- /dev/null +++ b/java/expression/generic/GenericTest.java @@ -0,0 +1,184 @@ +package expression.generic; + +import base.Selector; + +import java.math.BigInteger; +import java.util.function.*; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class GenericTest { + // === Base + private static final Consumer ADD = binary("+", 200); + private static final Consumer SUBTRACT = binary("-", -200); + private static final Consumer MULTIPLY = binary("*", 301); + private static final Consumer DIVIDE = binary("/", -300); + private static final Consumer NEGATE = unary("-"); + + // === Cmm + private static final Consumer COUNT = unary("count"); + private static final Consumer MIN = binary("min", 50); + private static final Consumer MAX = binary("max", 50); + + // === Checked integers + private static Integer i(final long v) { + if (v != (int) v) { + throw new ArithmeticException("Overflow"); + } + return (int) v; + } + + private static final GenericTester.Mode.Builder INTEGER_CHECKED = mode("i", c -> c) + .binary("+", (a, b) -> i(a + (long) b)) + .binary("-", (a, b) -> i(a - (long) b)) + .binary("*", (a, b) -> i(a * (long) b)) + .binary("/", (a, b) -> i(a / (long) b)) + .unary("-", a -> i(- (long) a)) + + .unary("count", Integer::bitCount) + .binary("min", Math::min) + .binary("max", Math::max) + ; + + // === Doubles + + private static final GenericTester.Mode.Builder DOUBLE = mode("d", c -> (double) c) + .binary("+", Double::sum) + .binary("-", (a, b) -> a - b) + .binary("*", (a, b) -> a * b) + .binary("/", (a, b) -> a / b) + .unary("-", a -> -a) + + .unary("count", a -> (double) Long.bitCount(Double.doubleToLongBits(a))) + .binary("min", Math::min) + .binary("max", Math::max) + ; + + // === BigIntegers + + private static final GenericTester.Mode.Builder BIG_INTEGER = mode("bi", BigInteger::valueOf) + .binary("+", BigInteger::add) + .binary("-", BigInteger::subtract) + .binary("*", BigInteger::multiply) + .binary("/", BigInteger::divide) + .unary("-", BigInteger::negate) + + .unary("count", a -> BigInteger.valueOf(a.bitCount())) + .binary("min", BigInteger::min) + .binary("max", BigInteger::max) + ; + + + // === Unchecked integers + + private static final GenericTester.Mode.Builder INTEGER_UNCHECKED = mode("u", c -> c) + .binary("+", Integer::sum) + .binary("-", (a, b) -> a - b) + .binary("*", (a, b) -> a * b) + .binary("/", (a, b) -> a / b) + .unary("-", a -> -a) + + .unary("count", Integer::bitCount) + .binary("min", Math::min) + .binary("max", Math::max) + ; + + + // === Short + + private static short s(final int x) { + return (short) x; + } + + private static BinaryOperator s(final IntBinaryOperator op) { + return (a, b) -> s(op.applyAsInt(a, b)); + } + + private static final GenericTester.Mode.Builder SHORT = mode("s", c -> (short) c, c -> (short) c) + .binary("+", s(Integer::sum)) + .binary("-", s((a, b) -> a - b)) + .binary("*", s((a, b) -> a * b)) + .binary("/", s((a, b) -> a / b)) + .unary("-", a -> s(-a)) + + .unary("count", a -> s(Integer.bitCount(a & 0xffff))) + .binary("min", s(Math::min)) + .binary("max", s(Math::max)) + ; + + // == Floats + + private static BinaryOperator f(final IntPredicate p) { + return (a, b) -> p.test(a.compareTo(b)) ? 1.0f : 0.0f; + } + + + private static final GenericTester.Mode.Builder FLOAT = mode("f", c -> (float) c) + .binary("+", Float::sum) + .binary("-", (a, b) -> a - b) + .binary("*", (a, b) -> a * b) + .binary("/", (a, b) -> a / b) + .unary("-", a -> -a) + + .unary("count", a -> (float) Integer.bitCount(Float.floatToIntBits(a))) + .binary("min", Math::min) + .binary("max", Math::max) + ; + + + + + // === Truncated integers + + /* package-private */ static final int TRUNCATE = 10; + private static int it(final int v) { + return v / TRUNCATE * TRUNCATE; + } + private static final GenericTester.Mode.Builder INTEGER_TRUNCATE = mode("it", GenericTest::it) + .binary("+", (a, b) -> it(a + b)) + .binary("-", (a, b) -> it(a - b)) + .binary("*", (a, b) -> it(a * b)) + .binary("/", (a, b) -> it(a / b)) + .unary("-", a -> it(-a)) + + .unary("count", a -> it(Integer.bitCount(a))) + .binary("min", Math::min) + .binary("max", Math::max) + ; + + + // === Common + + private GenericTest() { + } + + /* package-private */ static Consumer unary(final String name) { + return tester -> tester.unary(name, 1); + } + + /* package-private */ static Consumer binary(final String name, final int priority) { + return tester -> tester.binary(name, priority); + } + + public static final Selector SELECTOR = Selector.composite(GenericTest.class, GenericTester::new, "easy", "hard") + .variant("Base", INTEGER_CHECKED, DOUBLE, BIG_INTEGER, ADD, SUBTRACT, MULTIPLY, DIVIDE, NEGATE) + .variant("3637", INTEGER_UNCHECKED, SHORT, FLOAT, COUNT, MIN, MAX) + .variant("3839", INTEGER_UNCHECKED, SHORT, INTEGER_TRUNCATE, COUNT, MIN, MAX) + .variant("3435", INTEGER_UNCHECKED, COUNT, MIN, MAX) + .variant("3233", INTEGER_UNCHECKED, SHORT, FLOAT) + .selector(); + + private static GenericTester.Mode.Builder mode(final String mode, final IntFunction constant) { + return GenericTester.Mode.builder(mode, constant, IntUnaryOperator.identity()); + } + + private static GenericTester.Mode.Builder mode(final String mode, final IntFunction constant, final IntUnaryOperator fixer) { + return GenericTester.Mode.builder(mode, constant, fixer); + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} + diff --git a/java/expression/generic/GenericTester.java b/java/expression/generic/GenericTester.java new file mode 100644 index 0000000..1cf896c --- /dev/null +++ b/java/expression/generic/GenericTester.java @@ -0,0 +1,366 @@ +package expression.generic; + +import base.*; +import expression.ToMiniString; +import expression.common.*; +import expression.parser.ParserTestSet; +import java.util.*; +import java.util.function.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class GenericTester extends Tester { + + private static final int SIZE = 10; + private static final int MAX = Integer.MAX_VALUE - 1; + private static final int MIN = Integer.MIN_VALUE; + private static final List>> VARIABLES = List.of( + Pair.of("x", (x, y, z) -> x), + Pair.of("y", (x, y, z) -> y), + Pair.of("z", (x, y, z) -> z) + ); + private static final ExpressionKind.Variables> VARS = (c, r) -> + VARIABLES; + + protected final List>> tests = new ArrayList<>(); + private final Tabulator tabulator = new GenericTabulator(); + private final Set operations = new HashSet<>(); + private final List parens = new ArrayList<>( + List.of(NodeRenderer.paren("(", ")")) + ); + + private final TestGeneratorBuilder generator; + private final List> modes = new ArrayList<>(); + + public GenericTester(final TestCounter counter) { + super(counter); + generator = new TestGeneratorBuilder<>( + random(), + random()::nextInt, + ParserTestSet.CONSTS, + false + ); + } + + protected void test( + final String expression, + final String name, + final IF f + ) { + tests.add(Named.of(name + ": " + expression, f)); + } + + @Override + public void test() { + final List modes = this.modes.stream() + .map(mode -> mode.build(this)) + .toList(); + + for (final Named> test : tests) { + final String[] parts = test.name().split(": "); + testFull(parts[0], parts[1], test.value()); + } + + final TestGenerator> generator = this.generator.build( + VARS + ); + counter.scope("basic", () -> generator.testBasic(test(true, modes))); + counter.scope("random", () -> + generator.testRandom( + counter, + 20 + (TestCounter.DENOMINATOR - 1) * 2, + test(false, modes) + ) + ); + } + + private void testFull( + final String mode, + final String expression, + final IF f + ) { + testShort(mode, expression, f); + test(mode, expression, f, MAX, -1, MAX, 0); + test(mode, expression, f, MIN, 0, MIN, 1); + } + + private void testShort( + final String mode, + final String expression, + final IF f + ) { + test(mode, expression, f, 0, -1, 0, 1); + } + + private void test( + final String mode, + final String expression, + final IF f, + final int min, + final int dMin, + final int max, + final int dMax + ) { + test( + mode, + expression, + f, + min + random().nextInt(SIZE) * dMin, + max + random().nextInt(SIZE) * dMax, + min + random().nextInt(SIZE) * dMin, + max + random().nextInt(SIZE) * dMax, + min + random().nextInt(SIZE) * dMin, + max + random().nextInt(SIZE) * dMax + ); + } + + private Consumer>> test( + final boolean full, + final List modes + ) { + final NodeRenderer.Settings settings = NodeRenderer.FULL.withParens( + parens + ); + return test -> + modes.forEach(mode -> + mode.test(test.expr, test.render(settings), full) + ); + } + + private void test( + final String mode, + final String expression, + final IF f, + final int x1, + final int x2, + final int y1, + final int y2, + final int z1, + final int z2 + ) { + final String context = String.format( + "mode=%s, x=[%d, %d] y=[%d, %d] z=[%d, %d], expression=%s%n", + mode, + x1, + x2, + y1, + y2, + z1, + z2, + expression + ); + final Object[][][] result = counter.testV(() -> + TestCounter.get(() -> + tabulator.tabulate(mode, expression, x1, x2, y1, y2, z1, z2) + ).either( + e -> counter.fail(e, "%s %s", "tabulate", context), + Function.identity() + ) + ); + IntStream.rangeClosed(x1, x2).forEach(x -> + IntStream.rangeClosed(y1, y2).forEach(y -> + IntStream.rangeClosed(z1, z2).forEach(z -> + counter.test(() -> { + final Object expected = TestCounter.get(() -> + f.apply(x, y, z) + ).either(e -> null, Function.identity()); + final Object actual = result[x - x1][y - y1][z - z1]; + counter.checkTrue( + Objects.equals(actual, expected), + "table[%d][%d][%d](x=%d, y=%d, z=%d]) = %s (expected %s)%n%s", + x - x1, + y - y1, + z - z1, + x, + y, + z, + actual, + expected, + context + ); + }) + ) + ) + ); + } + + public void binary(final String name, final int priority) { + operations.add(name + ":2"); + generator.binary(name, priority); + } + + public void unary(final String name, final int priority) { + operations.add(name + ":1"); + generator.unary(name, priority); + } + + public void parens(final String... parens) { + assert parens.length % 2 == 0 : "Parens should come in pairs"; + for (int i = 0; i < parens.length; i += 2) { + this.parens.add(NodeRenderer.paren(parens[i], parens[i + 1])); + } + } + + /* package-private */ interface Mode { + static Builder builder( + final String mode, + final IntFunction constant, + final IntUnaryOperator fixer + ) { + return new Builder<>(mode, constant, fixer); + } + + void test( + final Expr> expr, + final String expression, + final boolean full + ); + + /* package-private */ final class Builder< + T + > implements Consumer { + + private final String mode; + private final IntFunction constant; + private final IntUnaryOperator fixer; + private final List>>> unary = + new ArrayList<>(); + private final List< + Named>> + > binary = new ArrayList<>(); + + private Builder( + final String mode, + final IntFunction constant, + final IntUnaryOperator fixer + ) { + this.mode = mode; + this.constant = constant; + this.fixer = fixer; + } + + public Builder unary( + final String name, + final UnaryOperator op + ) { + unary.add( + Named.of( + name, + arg -> (x, y, z) -> op.apply(arg.apply(x, y, z)) + ) + ); + return this; + } + + public Builder binary( + final String name, + final BinaryOperator op + ) { + binary.add( + Named.of( + name, + (a, b) -> + (x, y, z) -> + op.apply(a.apply(x, y, z), b.apply(x, y, z)) + ) + ); + return this; + } + + @Override + public void accept(final GenericTester tester) { + tester.modes.add(this); + } + + private Mode build(final GenericTester tester) { + final Set ops = Stream.concat( + unary.stream().map(op -> op.name() + ":1"), + binary.stream().map(op -> op.name() + ":2") + ).collect(Collectors.toUnmodifiableSet()); + final List diff = tester.operations + .stream() + .filter(Predicate.not(ops::contains)) + .toList(); + Asserts.assertTrue( + String.format("Missing operations for %s: %s", mode, diff), + diff.isEmpty() + ); + + final Renderer.Builder< + Integer, + Unit, + GenericTester.F + > builder = Renderer.builder( + value -> (x, y, z) -> constant.apply(value) + ); + unary.forEach(op -> + builder.unary(op.name(), (unit, arg) -> + op.value().apply(arg) + ) + ); + binary.forEach(op -> + builder.binary(op.name(), (unit, a, b) -> + op.value().apply(a, b) + ) + ); + final Renderer> renderer = builder.build(); + final TestGenerator> genRenderer = + tester.generator.build(VARS); + + return (expr, expression, full) -> { + final String fixed = + fixer == IntUnaryOperator.identity() + ? expression + : genRenderer.render(cata(expr), NodeRenderer.FULL); + @SuppressWarnings("unchecked") + final Expr> converted = (Expr< + Integer, + F + >) expr; + final F expected = renderer.render( + converted, + Unit.INSTANCE + ); + final IF f = (x, y, z) -> + expected.apply( + constant.apply(x), + constant.apply(y), + constant.apply(z) + ); + if (full) { + tester.testFull(mode, fixed, f); + } else { + tester.testShort(mode, fixed, f); + } + }; + } + + private Expr> cata( + final Expr> expr + ) { + return expr.node(node -> + node.cata( + c -> Node.constant(fixer.applyAsInt(c)), + Node::op, + Node::op, + Node::op + ) + ); + } + } + } + + @FunctionalInterface + protected interface IF { + T apply(int x, int y, int z); + } + + @FunctionalInterface + protected interface F extends ToMiniString { + T apply(T x, T y, T z); + } +} diff --git a/java/expression/generic/IntCheckedType.java b/java/expression/generic/IntCheckedType.java new file mode 100644 index 0000000..60d34ea --- /dev/null +++ b/java/expression/generic/IntCheckedType.java @@ -0,0 +1,71 @@ +package expression.generic; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class IntCheckedType implements ArithmeticType { + + public static final IntCheckedType INSTANCE = new IntCheckedType(); + + private IntCheckedType() {} + + @Override + public Integer fromInt(int value) { + return value; + } + + @Override + public Integer add(Integer a, Integer b) { + long result = (long) a + b; + checkOverflow(result); + return (int) result; + } + + @Override + public Integer subtract(Integer a, Integer b) { + long result = (long) a - b; + checkOverflow(result); + return (int) result; + } + + @Override + public Integer multiply(Integer a, Integer b) { + long result = (long) a * b; + checkOverflow(result); + return (int) result; + } + + @Override + public Integer divide(Integer a, Integer b) { + if (b == 0) throw new ArithmeticException("Division by zero"); + if (a == Integer.MIN_VALUE && b == -1) throw new ArithmeticException( + "Overflow" + ); + return a / b; + } + + @Override + public Integer negate(Integer a) { + if (a == Integer.MIN_VALUE) throw new ArithmeticException("Overflow"); + return -a; + } + + @Override + public Integer count(Integer a) { + return Integer.bitCount(a); + } + + @Override + public Integer min(Integer a, Integer b) { + return Math.min(a, b); + } + + @Override + public Integer max(Integer a, Integer b) { + return Math.max(a, b); + } + + private static void checkOverflow(long result) { + if (result != (int) result) throw new ArithmeticException("Overflow"); + } +} diff --git a/java/expression/generic/IntTruncType.java b/java/expression/generic/IntTruncType.java new file mode 100644 index 0000000..194ee94 --- /dev/null +++ b/java/expression/generic/IntTruncType.java @@ -0,0 +1,61 @@ +package expression.generic; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class IntTruncType implements ArithmeticType { + + public static final IntTruncType INSTANCE = new IntTruncType(); + + private IntTruncType() {} + + private static int t(int v) { + return v - (v % 10); + } + + @Override + public Integer fromInt(int value) { + return t(value); + } + + @Override + public Integer add(Integer a, Integer b) { + return t(a + b); + } + + @Override + public Integer subtract(Integer a, Integer b) { + return t(a - b); + } + + @Override + public Integer multiply(Integer a, Integer b) { + return t(a * b); + } + + @Override + public Integer divide(Integer a, Integer b) { + if (b == 0) throw new ArithmeticException("Division by zero"); + return t(a / b); + } + + @Override + public Integer negate(Integer a) { + return t(-a); + } + + @Override + public Integer count(Integer a) { + return t(Integer.bitCount(a)); + } + + @Override + public Integer min(Integer a, Integer b) { + return t(Math.min(a, b)); + } + + @Override + public Integer max(Integer a, Integer b) { + return t(Math.max(a, b)); + } +} diff --git a/java/expression/generic/IntUncheckedType.java b/java/expression/generic/IntUncheckedType.java new file mode 100644 index 0000000..5d0db40 --- /dev/null +++ b/java/expression/generic/IntUncheckedType.java @@ -0,0 +1,57 @@ +package expression.generic; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class IntUncheckedType implements ArithmeticType { + + public static final IntUncheckedType INSTANCE = new IntUncheckedType(); + + private IntUncheckedType() {} + + @Override + public Integer fromInt(int value) { + return value; + } + + @Override + public Integer add(Integer a, Integer b) { + return a + b; + } + + @Override + public Integer subtract(Integer a, Integer b) { + return a - b; + } + + @Override + public Integer multiply(Integer a, Integer b) { + return a * b; + } + + @Override + public Integer divide(Integer a, Integer b) { + if (b == 0) throw new ArithmeticException("Division by zero"); + return a / b; + } + + @Override + public Integer negate(Integer a) { + return -a; + } + + @Override + public Integer count(Integer a) { + return Integer.bitCount(a); + } + + @Override + public Integer min(Integer a, Integer b) { + return Math.min(a, b); + } + + @Override + public Integer max(Integer a, Integer b) { + return Math.max(a, b); + } +} diff --git a/java/expression/generic/ShortType.java b/java/expression/generic/ShortType.java new file mode 100644 index 0000000..a636be8 --- /dev/null +++ b/java/expression/generic/ShortType.java @@ -0,0 +1,57 @@ +package expression.generic; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class ShortType implements ArithmeticType { + + public static final ShortType INSTANCE = new ShortType(); + + private ShortType() {} + + @Override + public Short fromInt(int value) { + return (short) value; + } + + @Override + public Short add(Short a, Short b) { + return (short) (a + b); + } + + @Override + public Short subtract(Short a, Short b) { + return (short) (a - b); + } + + @Override + public Short multiply(Short a, Short b) { + return (short) (a * b); + } + + @Override + public Short divide(Short a, Short b) { + if (b == 0) throw new ArithmeticException("Division by zero"); + return (short) (a / b); + } + + @Override + public Short negate(Short a) { + return (short) (-a); + } + + @Override + public Short count(Short a) { + return (short) Integer.bitCount(a & 0xffff); + } + + @Override + public Short min(Short a, Short b) { + return (short) Math.min(a, b); + } + + @Override + public Short max(Short a, Short b) { + return (short) Math.max(a, b); + } +} diff --git a/java/expression/generic/Tabulator.java b/java/expression/generic/Tabulator.java new file mode 100644 index 0000000..847df4f --- /dev/null +++ b/java/expression/generic/Tabulator.java @@ -0,0 +1,18 @@ +package expression.generic; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +public interface Tabulator { + Object[][][] tabulate( + String mode, + String expression, + int x1, + int x2, + int y1, + int y2, + int z1, + int z2 + ) throws Exception; +} diff --git a/java/expression/generic/package-info.java b/java/expression/generic/package-info.java new file mode 100644 index 0000000..648797d --- /dev/null +++ b/java/expression/generic/package-info.java @@ -0,0 +1,8 @@ +/** + * Tests for Generic Expressions homework + * of Paradigms of Programming course. + * + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression.generic; \ No newline at end of file diff --git a/java/expression/package-info.java b/java/expression/package-info.java new file mode 100644 index 0000000..7c1c31c --- /dev/null +++ b/java/expression/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Expressions homework + * of Paradigms of Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression; \ No newline at end of file diff --git a/java/expression/parser/Operations.java b/java/expression/parser/Operations.java new file mode 100644 index 0000000..00d5d44 --- /dev/null +++ b/java/expression/parser/Operations.java @@ -0,0 +1,68 @@ +package expression.parser; + +import expression.ToMiniString; +import expression.common.ExpressionKind; +import expression.common.Reason; + +import java.util.function.*; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Operations { + // === Base + + public static final Operation NEGATE = unary("-", 1, a -> -a); + @SuppressWarnings("Convert2MethodRef") + public static final Operation ADD = binary("+", 1600, (a, b) -> a + b); + public static final Operation SUBTRACT = binary("-", 1602, (a, b) -> a - b); + public static final Operation MULTIPLY = binary("*", 2001, (a, b) -> a * b); + public static final Operation DIVIDE = binary("/", 2002, (a, b) -> b == 0 ? Reason.DBZ.error() : a / b); + + + // === 3637 + public static final Operation ABS = unary("‖", "‖", Math::abs); + public static final Operation CUBE = unary("³", 2, (a, c) -> c.applyAsInt(a * a) * a); + public static final Operation CBRT = unary("∛", 1, a -> (long) Math.cbrt(a)); + + // === 3839 + private static final Reason NEGATIVE_SQRT = new Reason("Square root of negative value"); + public static final Operation SQUARE = unary("²", 2, a -> a * a); + public static final Operation SQRT = unary("√", 1, NEGATIVE_SQRT.less(0, a -> (long) Math.sqrt(a))); + + + // === Common + + private Operations() { + } + + public static Operation unary(final String name, final int priority, final LongUnaryOperator op) { + return unary(name, priority, (a, c) -> op.applyAsLong(a)); + } + + public static Operation unary(final String left, final String right, final LongUnaryOperator op) { + return unary(left, right, (a, c) -> op.applyAsLong(a)); + } + + public static Operation unary(final String name, final int priority, final BiFunction op) { + return tests -> tests.unary(name, priority, op); + } + + public static Operation unary(final String left, final String right, final BiFunction op) { + return tests -> tests.unary(left, right, op); + } + + public static Operation binary(final String name, final int priority, final LongBinaryOperator op) { + return tests -> tests.binary(name, priority, op); + } + + public static Operation kind( + final ExpressionKind kind, + final ParserTestSet.Parser parser + ) { + return factory -> factory.kind(kind, parser); + } + + @FunctionalInterface + public interface Operation extends Consumer {} +} diff --git a/java/expression/parser/ParserTestSet.java b/java/expression/parser/ParserTestSet.java new file mode 100644 index 0000000..c70f039 --- /dev/null +++ b/java/expression/parser/ParserTestSet.java @@ -0,0 +1,236 @@ +package expression.parser; + +import base.*; +import expression.ToMiniString; +import expression.common.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ParserTestSet { + private static final int D = 5; + + private static final List TEST_VALUES = new ArrayList<>(); + static { + Functional.addRange(TEST_VALUES, D, D); + Functional.addRange(TEST_VALUES, D, -D); + } + + public static final List CONSTS + = List.of(0, 1, -1, 4, -4, 10, -10, 30, -30, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUE); + + protected final ParserTester tester; + protected final ParsedKind kind; + private final boolean safe; + + protected final TestCounter counter; + + public ParserTestSet(final ParserTester tester, final ParsedKind kind) { + this(tester, kind, true); + } + + protected ParserTestSet(final ParserTester tester, final ParsedKind kind, final boolean safe) { + this.tester = tester; + this.kind = kind; + this.safe = safe; + + counter = tester.getCounter(); + } + + private void examples(final TestGenerator generator) { + example(generator, "$x+2", (x, y, z) -> x + 2); + example(generator, "2-$y", (x, y, z) -> 2 - y); + example(generator, " 3* $z ", (x, y, z) -> 3 * z); + example(generator, "$x/ - 2", (x, y, z) -> -x / 2); + example(generator, "$x*$y+($z-1 )/10", (x, y, z) -> x * y + (int) (z - 1) / 10); + example(generator, "-(-(-\t\t-5 + 16 *$x*$y) + 1 * $z) -(((-11)))", (x, y, z) -> -(-(5 + 16 * x * y) + z) + 11); + example(generator, "" + Integer.MAX_VALUE, (x, y, z) -> (long) Integer.MAX_VALUE); + example(generator, "" + Integer.MIN_VALUE, (x, y, z) -> (long) Integer.MIN_VALUE); + example(generator, "$x--$y--$z", (x, y, z) -> x + y + z); + example(generator, "((2+2))-0/(--2)*555", (x, y, z) -> 4L); + example(generator, "$x-$x+$y-$y+$z-($z)", (x, y, z) -> 0L); + example(generator, "(".repeat(300) + "$x + $y + (-10*-$z)" + ")".repeat(300), (x, y, z) -> x + y + 10 * z); + example(generator, "$x / $y / $z", (x, y, z) -> y == 0 || z == 0 ? Reason.DBZ.error() : (int) x / (int) y / z); + } + + private void example(final TestGenerator generator, final String expr, final ExampleExpression expression) { + final List names = Functional.map(generator.variables(3), Pair::first); + final TExpression expected = vars -> expression.evaluate(vars.get(0), vars.get(1), vars.get(2)); + + counter.test(() -> { + final String mangled = mangle(expr, names); + final E parsed = parse(mangled, names, true); + Functional.allValues(TEST_VALUES, 3).forEach(values -> check(expected, parsed, names, values, mangled)); + }); + } + + protected static String mangle(final String expr, final List names) { + return expr + .replace("$x", names.get(0)) + .replace("$y", names.get(1)) + .replace("$z", names.get(2)); + } + + protected void test() { + final TestGenerator generator = tester.generator.build(kind.kind.variables()); + final Renderer renderer = tester.renderer.build(); + final Consumer> consumer = test -> test(renderer, test); + counter.scope("Basic tests", () -> generator.testBasic(consumer)); + counter.scope("Handmade tests", () -> examples(generator)); + counter.scope("Random tests", () -> generator.testRandom(counter, 1, consumer)); + } + + private void test(final Renderer renderer, final TestGenerator.Test test) { + final Expr expr = test.expr; + final List> vars = expr.variables(); + final List variables = Functional.map(vars, Pair::first); + final String full = test.render(NodeRenderer.FULL); + final String mini = test.render(NodeRenderer.MINI); + + final E fullParsed = parse(test, variables, NodeRenderer.FULL); + final E miniParsed = parse(test, variables, NodeRenderer.MINI); + final E safeParsed = parse(test, variables, NodeRenderer.SAME); + + checkToString(full, mini, "base", fullParsed); + if (tester.mode() > 0) { + counter.test(() -> Asserts.assertEquals("mini.toMiniString", mini, miniParsed.toMiniString())); + counter.test(() -> Asserts.assertEquals("safe.toMiniString", mini, safeParsed.toMiniString())); + } + checkToString(full, mini, "extraParentheses", parse(test, variables, NodeRenderer.FULL_EXTRA)); + checkToString(full, mini, "noSpaces", parse(removeSpaces(full), variables, false)); + checkToString(full, mini, "extraSpaces", parse(extraSpaces(full), variables, false)); + + final TExpression expected = renderer.render( + Expr.of( + expr.node(), + Functional.map(vars, (i, var) -> Pair.of(var.first(), args -> args.get(i))) + ), + Unit.INSTANCE + ); + + check(expected, fullParsed, variables, tester.random().random(variables.size(), ExtendedRandom::nextInt), full); + if (this.safe) { + final String safe = test.render(NodeRenderer.SAME); + check(expected, safeParsed, variables, tester.random().random(variables.size(), ExtendedRandom::nextInt), safe); + } + } + + private E parse( + final TestGenerator.Test test, + final List variables, + final NodeRenderer.Settings settings + ) { + return parse(test.render(settings.withParens(tester.parens)), variables, false); + } + + private static final String LOOKBEHIND = "(?*/+=!-])"; + private static final String LOOKAHEAD = "(?![a-zA-Z0-9<>*/])"; + private static final Pattern SPACES = Pattern.compile(LOOKBEHIND + " | " + LOOKAHEAD + "|" + LOOKAHEAD + LOOKBEHIND); + private String extraSpaces(final String expression) { + return SPACES.matcher(expression).replaceAll(r -> tester.random().randomString( + ExtendedRandom.SPACES, + tester.random().nextInt(5) + )); + } + + private static String removeSpaces(final String expression) { + return SPACES.matcher(expression).replaceAll(""); + } + + private void checkToString(final String full, final String mini, final String context, final ToMiniString parsed) { + counter.test(() -> { + assertEquals(context + ".toString", full, full, parsed.toString()); + if (tester.mode() > 0) { + assertEquals(context + ".toMiniString", full, mini, parsed.toMiniString()); + } + }); + } + + private static void assertEquals( + final String context, + final String original, + final String expected, + final String actual + ) { + final String message = String.format("%s:%n original `%s`,%n expected `%s`,%n actual `%s`", + context, original, expected, actual); + Asserts.assertTrue(message, Objects.equals(expected, actual)); + } + + private Either eval(final TExpression expression, final List vars) { + return Reason.eval(() -> tester.cast(expression.evaluate(vars))); + } + + protected E parse(final String expression, final List variables, final boolean reparse) { + return counter.testV(() -> { + final E parsed = counter.testV(() -> counter.call("parse", + () -> kind.parse(expression, variables))); + if (reparse) { + counter.testV(() -> counter.call("parse", () -> kind.parse(parsed.toString(), variables))); + } + return parsed; + }); + } + + private void check( + final TExpression expectedExpression, + final E expression, + final List variables, + final List values, + final String unparsed + ) { + counter.test(() -> { + final Either answer = eval(expectedExpression, values); + final String args = IntStream.range(0, variables.size()) + .mapToObj(i -> variables.get(i) + "=" + values.get(i)) + .collect(Collectors.joining(", ")); + final String message = String.format("f(%s)%n\twhere f=%s%n\tyour f=%s", args, unparsed, expression); + try { + final C actual = kind.kind.evaluate(expression, variables, kind.kind.fromInts(values)); + counter.checkTrue(answer.isRight(), "Error expected for f(%s)%n\twhere f=%s%n\tyour f=%s", args, unparsed, expression); + Asserts.assertEquals(message, answer.getRight(), actual); + } catch (final Exception e) { + if (answer.isRight()) { + counter.fail(e, "No error expected for %s", message); + } + } + }); + } + + @FunctionalInterface + public interface TExpression { + long evaluate(List vars); + } + + @FunctionalInterface + protected interface ExampleExpression { + long evaluate(long x, long y, long z); + } + + /** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ + public record ParsedKind(ExpressionKind kind, Parser parser) { + public E parse(final String expression, final List variables) throws Exception { + return parser.parse(expression, variables); + } + + @Override + public String toString() { + return kind.toString(); + } + } + + @FunctionalInterface + public interface Parser { + E parse(final String expression, final List variables) throws Exception; + } +} diff --git a/java/expression/parser/ParserTester.java b/java/expression/parser/ParserTester.java new file mode 100644 index 0000000..059acf2 --- /dev/null +++ b/java/expression/parser/ParserTester.java @@ -0,0 +1,76 @@ +package expression.parser; + +import base.ExtendedRandom; +import base.TestCounter; +import base.Tester; +import base.Unit; +import expression.ToMiniString; +import expression.common.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.LongBinaryOperator; +import java.util.function.LongToIntFunction; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ParserTester extends Tester { + /* package-private */ final TestGeneratorBuilder generator; + /* package-private */ final Renderer.Builder renderer; + private final List> kinds = new ArrayList<>(); + /* package-private */ final List parens = new ArrayList<>(List.of(NodeRenderer.paren("(", ")"))); + + public ParserTester(final TestCounter counter) { + super(counter); + renderer = Renderer.builder(c -> vars -> c); + final ExtendedRandom random = counter.random(); + generator = new TestGeneratorBuilder<>(random, random::nextInt, ParserTestSet.CONSTS, true); + } + + public void unary(final String name, final int priority, final BiFunction op) { + generator.unary(name, priority); + renderer.unary(name, (unit, a) -> vars -> cast(op.apply(a.evaluate(vars), this::cast))); + } + + public void unary(final String left, final String right, final BiFunction op) { + generator.unary(left, right); + renderer.unary(left, (unit, a) -> vars -> cast(op.apply(a.evaluate(vars), this::cast))); + } + + public void binary(final String name, final int priority, final LongBinaryOperator op) { + generator.binary(name, priority); + renderer.binary(name, (unit, a, b) -> vars -> cast(op.applyAsLong(a.evaluate(vars), b.evaluate(vars)))); + } + + void kind(final ExpressionKind kind, final ParserTestSet.Parser parser) { + kinds.add(new ParserTestSet.ParsedKind<>(kind, parser)); + } + + @Override + public void test() { + for (final ParserTestSet.ParsedKind kind : kinds) { + counter.scope(kind.toString(), () -> test(kind)); + } + } + + protected void test(final ParserTestSet.ParsedKind kind) { + new ParserTestSet<>(this, kind).test(); + } + + public TestCounter getCounter() { + return counter; + } + + protected int cast(final long value) { + return (int) value; + } + + public void parens(final String... parens) { + assert parens.length % 2 == 0 : "Parens should come in pairs"; + for (int i = 0; i < parens.length; i += 2) { + this.parens.add(NodeRenderer.paren(parens[i], parens[i + 1])); + } + } +} diff --git a/java/expression/parser/package-info.java b/java/expression/parser/package-info.java new file mode 100644 index 0000000..44078aa --- /dev/null +++ b/java/expression/parser/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Expressions Parsing homework + * of Paradigms of Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression.parser; \ No newline at end of file diff --git a/java/queue/AbstractQueue.java b/java/queue/AbstractQueue.java new file mode 100644 index 0000000..3938d59 --- /dev/null +++ b/java/queue/AbstractQueue.java @@ -0,0 +1,176 @@ +package queue; + +import java.util.function.Predicate; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public abstract class AbstractQueue implements Queue { + + protected int size; + + @Override + public int size() { + return size; + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public void clear() { + clearImpl(); + size = 0; + } + + @Override + public int count(Object element) { + assert element != null : "Element must not be null"; + int cnt = 0; + for (int i = 0; i < size; i++) { + if (element.equals(get(i))) cnt++; + } + return cnt; + } + + @Override + public int countIf(Predicate predicate) { + assert predicate != null : "Predicate must not be null"; + int cnt = 0; + for (int i = 0; i < size; i++) { + if (predicate.test(get(i))) cnt++; + } + return cnt; + } + + @Override + public int indexIf(Predicate predicate) { + assert predicate != null : "Predicate must not be null"; + for (int i = 0; i < size; i++) { + if (predicate.test(get(i))) return i; + } + return -1; + } + + @Override + public int lastIndexIf(Predicate predicate) { + assert predicate != null : "Predicate must not be null"; + for (int i = size - 1; i >= 0; i--) { + if (predicate.test(get(i))) return i; + } + return -1; + } + + @Override + public int indexOf(Object element) { + assert element != null : "Predicate must not be null"; + for (int i = 0; i < size; i++) { + if (get(i).equals(element)) return i; + } + return -1; + } + + @Override + public int lastIndexOf(Object element) { + assert element != null : "Predicate must not be null"; + for (int i = size - 1; i >= 0; i--) { + if (get(i).equals(element)) return i; + } + return -1; + } + + @Override + public boolean contains(Object element) { + assert element != null : "Element must not be null"; + return indexOf(element) != -1; + } + + @Override + public boolean removeFirst(Object element) { + assert element != null : "Element must not be null"; + int originalSize = size; + boolean found = false; + for (int i = 0; i < originalSize; i++) { + Object elem = dequeue(); + if (!found && element.equals(elem)) { + found = true; + } else { + enqueue(elem); + } + } + return found; + } + + @Override + public Queue getNth(int n) { + Queue result = createEmpty(); + for (int i = 0; i < size; i++) { + Object elem = get(i); + if ((i + 1) % n == 0) result.enqueue(elem); + } + return result; + } + + @Override + public Queue removeNth(int n) { + Queue result = createEmpty(); + int originalSize = size; + for (int i = 0; i < originalSize; i++) { + if ((i + 1) % n == 0) result.enqueue(get(i)); + } + Queue temp = createEmpty(); + for (int i = 0; i < originalSize; i++) { + Object elem = dequeue(); + if ((i + 1) % n != 0) temp.enqueue(elem); + } + while (!temp.isEmpty()) enqueue(temp.dequeue()); + return result; + } + + @Override + public void dropNth(int n) { + int originalSize = size; + Queue temp = createEmpty(); + for (int i = 0; i < originalSize; i++) { + Object elem = dequeue(); + if ((i + 1) % n != 0) temp.enqueue(elem); + } + while (!temp.isEmpty()) enqueue(temp.dequeue()); + } + + @Override + public void removeIf(java.util.function.Predicate predicate) { + assert predicate != null : "Predicate must not be null"; + int originalSize = size; + for (int i = 0; i < originalSize; i++) { + Object elem = dequeue(); + if (!predicate.test(elem)) enqueue(elem); + } + } + + @Override + public void retainIf(java.util.function.Predicate predicate) { + assert predicate != null : "Predicate must not be null"; + removeIf(predicate.negate()); + } + + @Override + public void removeAll(Object element) { + assert element != null : "Element must not be null"; + removeIf(element::equals); + } + + @Override + public void retainAll(Object element) { + assert element != null : "Element must not be null"; + removeIf(e -> !element.equals(e)); + } + + protected abstract AbstractQueue createEmpty(); + + protected abstract Object get(int i); + + protected abstract void clearImpl(); +} diff --git a/java/queue/ArrayQueue.java b/java/queue/ArrayQueue.java new file mode 100644 index 0000000..8c138a7 --- /dev/null +++ b/java/queue/ArrayQueue.java @@ -0,0 +1,90 @@ +package queue; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class ArrayQueue extends AbstractQueue { + + private Object[] queue; + private int head; + + public ArrayQueue() { + queue = new Object[4]; + head = 0; + } + + @Override + public void enqueue(Object element) { + assert element != null : "Element must not be null"; + ensureCapacity(); + queue[(head + size) % queue.length] = element; + size++; + } + + @Override + public void push(Object element) { + assert element != null : "Element must not be null"; + ensureCapacity(); + head = (head - 1 + queue.length) % queue.length; + queue[head] = element; + size++; + } + + @Override + public Object element() { + assert size > 0 : "Queue is empty"; + return queue[head]; + } + + @Override + public Object peek() { + assert size > 0 : "Queue is empty"; + return queue[(head + size - 1) % queue.length]; + } + + @Override + public Object dequeue() { + assert size > 0 : "Queue is empty"; + Object result = queue[head]; + queue[head] = null; + head = (head + 1) % queue.length; + size--; + return result; + } + + @Override + public Object remove() { + assert size > 0 : "Queue is empty"; + int tail = (head + size - 1) % queue.length; + Object result = queue[tail]; + queue[tail] = null; + size--; + return result; + } + + @Override + protected Object get(int i) { + return queue[(head + i) % queue.length]; + } + + @Override + protected void clearImpl() { + queue = new Object[4]; + head = 0; + } + + @Override + protected AbstractQueue createEmpty() { + return new ArrayQueue(); + } + + private void ensureCapacity() { + if (size < queue.length) return; + Object[] newQueue = new Object[queue.length * 2]; + for (int i = 0; i < size; i++) { + newQueue[i] = queue[(head + i) % queue.length]; + } + queue = newQueue; + head = 0; + } +} diff --git a/java/queue/ArrayQueueADT.java b/java/queue/ArrayQueueADT.java new file mode 100644 index 0000000..1e015eb --- /dev/null +++ b/java/queue/ArrayQueueADT.java @@ -0,0 +1,150 @@ +package queue; + +import java.util.function.Predicate; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class ArrayQueueADT { + private Object[] queue; + private int head; + private int size; + + public ArrayQueueADT() { + queue = new Object[4]; + head = 0; + size = 0; + } + + public static ArrayQueueADT create() { + return new ArrayQueueADT(); + } + + public static void enqueue(ArrayQueueADT q, Object element) { + assert q != null && element != null : "Queue and element must not be null"; + ensureCapacity(q); + q.queue[(q.head + q.size) % q.queue.length] = element; + q.size++; + } + + public static void push(ArrayQueueADT q, Object element) { + assert q != null && element != null : "Queue and element must not be null"; + ensureCapacity(q); + q.head = (q.head - 1 + q.queue.length) % q.queue.length; + q.queue[q.head] = element; + q.size++; + } + + public static Object element(ArrayQueueADT q) { + assert q != null && q.size > 0 : "Queue must not be null and not be empty"; + return q.queue[q.head]; + } + + public static Object peek(ArrayQueueADT q) { + assert q != null && q.size > 0 : "Queue must not be null and not be empty"; + return q.queue[(q.head + q.size - 1) % q.queue.length]; + } + + public static Object dequeue(ArrayQueueADT q) { + assert q != null && q.size > 0 : "Queue must not be null and not be empty"; + Object result = q.queue[q.head]; + q.queue[q.head] = null; + q.head = (q.head + 1) % q.queue.length; + q.size--; + return result; + } + + public static Object remove(ArrayQueueADT q) { + assert q != null && q.size > 0 : "Queue must not be null and not be empty"; + int tail = (q.head + q.size - 1) % q.queue.length; + Object result = q.queue[tail]; + q.queue[tail] = null; + q.size--; + return result; + } + + public static int size(ArrayQueueADT q) { + assert q != null : "Queue must not be null"; + return q.size; + } + + public static boolean isEmpty(ArrayQueueADT q) { + assert q != null : "Queue must not be null"; + return q.size == 0; + } + + public static void clear(ArrayQueueADT q) { + assert q != null : "Queue must not be null"; + q.queue = new Object[4]; + q.head = 0; + q.size = 0; + } + + public static int count(ArrayQueueADT q, Object element) { + assert q != null && element != null : "Queue and element must not be null"; + int cnt = 0; + for (int i = 0; i < q.size; i++) { + if (element.equals(q.queue[(q.head + i) % q.queue.length])) { + cnt++; + } + } + return cnt; + } + + public static int countIf(ArrayQueueADT q, Predicate predicate) { + assert q != null && predicate != null : "Queue and predicate must not be null"; + int cnt = 0; + for (int i = 0; i < q.size; i++) { + if (predicate.test(q.queue[(q.head + i) % q.queue.length])) { + cnt++; + } + } + return cnt; + } + + public static int indexIf(ArrayQueueADT q, Predicate predicate) { + assert q != null && predicate != null : "Queue and predicate must not be null"; + for (int i = 0; i < q.size; i++) { + if (predicate.test(q.queue[(q.head + i) % q.queue.length])) { + return i; + } + } + return -1; + } + + public static int lastIndexIf(ArrayQueueADT q, Predicate predicate) { + assert q != null && predicate != null : "Queue and predicate must not be null"; + for (int i = q.size - 1; i >= 0; i--) { + if (predicate.test(q.queue[(q.head + i) % q.queue.length])) { + return i; + } + } + return -1; + } + + public static int indexOf(ArrayQueueADT q, Object element) { + assert element != null : "Predicate must not be null"; + for (int i = 0; i < q.size; i++) { + if (q.queue[(q.head + i) % q.queue.length].equals(element)) return i; + } + return -1; + } + + public static int lastIndexOf(ArrayQueueADT q, Object element) { + assert element != null : "Predicate must not be null"; + for (int i = q.size - 1; i >= 0; i--) { + if (q.queue[(q.head + i) % q.queue.length].equals(element)) return i; + } + return -1; + } + + private static void ensureCapacity(ArrayQueueADT q) { + if (q.size < q.queue.length) return; + Object[] newQueue = new Object[q.queue.length * 2]; + for (int i = 0; i < q.size; i++) { + newQueue[i] = q.queue[(q.head + i) % q.queue.length]; + } + q.queue = newQueue; + q.head = 0; + } +} \ No newline at end of file diff --git a/java/queue/ArrayQueueModule.java b/java/queue/ArrayQueueModule.java new file mode 100644 index 0000000..5b06d3d --- /dev/null +++ b/java/queue/ArrayQueueModule.java @@ -0,0 +1,137 @@ +package queue; + +import java.util.function.Predicate; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class ArrayQueueModule { + private static Object[] queue = new Object[4]; + private static int head = 0; + private static int size = 0; + + public static void enqueue(Object element) { + assert element != null : "Element must not be null"; + ensureCapacity(); + queue[(head + size) % queue.length] = element; + size++; + } + + public static void push(Object element) { + assert element != null : "Element must not be null"; + ensureCapacity(); + head = (head - 1 + queue.length) % queue.length; + queue[head] = element; + size++; + } + + public static Object element() { + assert size > 0 : "Queue is empty"; + return queue[head]; + } + + public static Object peek() { + assert size > 0 : "Queue is empty"; + return queue[(head + size - 1) % queue.length]; + } + + public static Object dequeue() { + assert size > 0 : "Queue is empty"; + Object result = queue[head]; + queue[head] = null; + head = (head + 1) % queue.length; + size--; + return result; + } + + public static Object remove() { + assert size > 0 : "Queue is empty"; + int tail = (head + size - 1) % queue.length; + Object result = queue[tail]; + queue[tail] = null; + size--; + return result; + } + + public static int size() { + return size; + } + + public static boolean isEmpty() { + return size == 0; + } + + public static void clear() { + queue = new Object[4]; + head = 0; + size = 0; + } + + public static int count(Object element) { + assert element != null : "Element must not be null"; + int cnt = 0; + for (int i = 0; i < size; i++) { + if (element.equals(queue[(head + i) % queue.length])) { + cnt++; + } + } + return cnt; + } + + public static int countIf(Predicate predicate) { + assert predicate != null : "Predicate must not be null"; + int cnt = 0; + for (int i = 0; i < size; i++) { + if (predicate.test(queue[(head + i) % queue.length])) { + cnt++; + } + } + return cnt; + } + + public static int indexIf(Predicate predicate) { + assert predicate != null : "Predicate must not be null"; + for (int i = 0; i < size; i++) { + if (predicate.test(queue[(head + i) % queue.length])) { + return i; + } + } + return -1; + } + + public static int lastIndexIf(Predicate predicate) { + assert predicate != null : "Predicate must not be null"; + for (int i = size - 1; i >= 0; i--) { + if (predicate.test(queue[(head + i) % queue.length])) { + return i; + } + } + return -1; + } + + public static int indexOf(Object element) { + assert element != null : "Predicate must not be null"; + for (int i = 0; i < size; i++) { + if (queue[(head + i) % queue.length].equals(element)) return i; + } + return -1; + } + + public static int lastIndexOf(Object element) { + assert element != null : "Predicate must not be null"; + for (int i = size - 1; i >= 0; i--) { + if (queue[(head + i) % queue.length].equals(element)) return i; + } + return -1; + } + + private static void ensureCapacity() { + if (size < queue.length) return; + Object[] newQueue = new Object[queue.length * 2]; + for (int i = 0; i < size; i++) { + newQueue[i] = queue[(head + i) % queue.length]; + } + queue = newQueue; + head = 0; + } +} \ No newline at end of file diff --git a/java/queue/ArrayQueueTest.java b/java/queue/ArrayQueueTest.java new file mode 100644 index 0000000..e5cf0c2 --- /dev/null +++ b/java/queue/ArrayQueueTest.java @@ -0,0 +1,46 @@ +package queue; + +import base.Selector; +import base.TestCounter; + +import java.util.List; +import java.util.function.Consumer; + +import static queue.Queues.*; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ArrayQueueTest { + public static final Selector SELECTOR = new Selector(ArrayQueueTest.class) + .variant("Base", variant(QueueModel.class, d -> () -> d)) + .variant("3637", variant(DequeCountModel.class, (DequeChecker) d -> () -> d, DEQUE_COUNT)) + .variant("3839", variant(DequeCountIfModel.class, (DequeChecker) d -> () -> d, DEQUE_COUNT_IF)) + .variant("3435", variant(IndexIfModel.class, d -> () -> d, INDEX_IF)) + .variant("3233", variant(IndexModel.class, d -> () -> d, INDEX)) + ; + + private ArrayQueueTest() { + } + + public static void main(final String... args) { + SELECTOR.main(args); + } + + /* package-private */ + static Consumer variant( + final Class type, + final QueueChecker tester, + final Splitter splitter + ) { + return new ArrayQueueTester<>(type, tester, splitter)::test; + } + + /* package-private */ + static Consumer variant( + final Class type, + final QueueChecker tester + ) { + return variant(type, tester, (t, q, r) -> List.of()); + } +} diff --git a/java/queue/ArrayQueueTester.java b/java/queue/ArrayQueueTester.java new file mode 100644 index 0000000..da608d1 --- /dev/null +++ b/java/queue/ArrayQueueTester.java @@ -0,0 +1,224 @@ +package queue; + +import base.Asserts; +import base.ExtendedRandom; +import base.TestCounter; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ArrayQueueTester { + private static final int OPERATIONS = 50_000; + /* package-private */ static final Object[] ELEMENTS = new Object[]{ + "Hello", + "world", + 1, 2, 3, + List.of("a"), + List.of("a"), + List.of("b"), + Map.of() + }; + + private final Class model; + private final Queues.QueueChecker tester; + + protected ArrayQueueTester(final Class model, final Queues.QueueChecker tester, final Queues.Splitter splitter) { + this.model = model; + this.tester = new Queues.QueueChecker<>() { + @Override + public M wrap(final ArrayDeque reference) { + return tester.wrap(reference); + } + + @Override + public void check(final M queue, final ExtendedRandom random) { + tester.check(queue, random); + } + + @Override + public void add(final M queue, final Object element, final ExtendedRandom random) { + tester.add(queue, element, random); + } + + @Override + public void remove(final M queue, final ExtendedRandom random) { + tester.remove(queue, random); + } + + @Override + public List linearTest(final M queue, final ExtendedRandom random) { + if (random.nextInt(50) == 0) { + queue.clear(); + return List.of(); + } else { + return splitter.split(this, queue, random); + } + } + }; + } + + protected void test(final TestCounter counter) { + test(counter, "ArrayQueue", ReflectionTest.Mode.values()); + } + + protected void test(final TestCounter counter, final String className, final ReflectionTest.Mode... modes) { + for (final ReflectionTest.Mode mode : modes) { + final String scope = "Running %s for %s in %s mode%n" + .formatted(model.getEnclosingClass().getSimpleName(), className, mode); + counter.scope(scope, () -> new Variant(counter).test(className, mode)); + } + } + + + + protected void checkImplementation(final Class implementation) { + // Do nothing by default + } + + private static List toList(final Queues.QueueModel queue) { + final List list = Stream.generate(queue::dequeue).limit(queue.size()).toList(); + list.forEach(queue::enqueue); + return list; + } + + protected static ArrayDeque collect(final Stream elements) { + return elements.collect(Collectors.toCollection(ArrayDeque::new)); + } + + private class Variant extends ReflectionTest { + private final TestCounter counter; + + public Variant(final TestCounter counter) { + this.counter = counter; + } + + protected void testEmpty(final M queue) { + counter.scope("testEmpty", () -> assertSize(0, queue)); + } + + protected void testSingleton(final M queue) { + counter.scope("testSingleton", () -> { + assertSize(0, queue); + final String value = "value"; + queue.enqueue(value); + assertSize(1, queue); + Asserts.assertEquals("element()", value, queue.element()); + Asserts.assertEquals("dequeue()", value, queue.dequeue()); + assertSize(0, queue); + }); + } + + protected void testClear(final M queue) { + counter.scope("testClear", () -> { + assertSize(0, queue); + + final String value = "value"; + queue.enqueue(value); + queue.enqueue(value); + queue.clear(); + assertSize(0, queue); + + final String value1 = "value1"; + queue.enqueue(value1); + Asserts.assertEquals("deque()", value1, queue.dequeue()); + }); + } + + private int checkAndSize(final M queue) { + final int size = queue.size(); + if (!queue.isEmpty() && random().nextBoolean()) { + tester.check(queue, random()); + } + return size; + } + + protected Object randomElement() { + return ELEMENTS[random().nextInt(ELEMENTS.length)]; + } + + protected void assertSize(final int size, final M queue) { + counter.test(() -> { + Asserts.assertEquals("size()", size, queue.size()); + Asserts.assertTrue("Expected isEmpty() " + (size == 0) + ", found " + queue.isEmpty(), (size == 0) == queue.isEmpty()); + }); + } + + @Override + protected void checkResult(final String call, final Object expected, final Object actual) { + if (expected instanceof Queues.QueueModel model) { + super.checkResult(call, toList(model), toList((Queues.QueueModel) actual)); + } else { + super.checkResult(call, expected, actual); + } + } + + protected Supplier factory(final String name, final Mode mode) { + final ProxyFactory factory = new ProxyFactory<>(model, mode, "queue." + name); + checkImplementation(factory.implementation); + return () -> checking(counter, model, tester.wrap(new ArrayDeque<>()), factory.create()); + } + + private void test(final String className, final Mode mode) { + final Supplier factory = factory(className, mode); + testEmpty(factory.get()); + testSingleton(factory.get()); + testClear(factory.get()); + for (int i = 0; i <= 10; i += 2) { + testRandom(factory.get(), (double) i / 10); + } + } + + private void testRandom(final M initial, final double addFreq) { + counter.scope("testRandom, add frequency = " + addFreq, () -> { + final List queues = new ArrayList<>(); + queues.add(initial); + int ops = 0; + for (int i = 0; i < OPERATIONS / TestCounter.DENOMINATOR / TestCounter.DENOMINATOR; i++) { + final M queue = queues.get(random().nextInt(queues.size())); + + final int size = counter.testV(() -> { + if (queue.isEmpty() || random().nextDouble() < addFreq) { + tester.add(queue, randomElement(), random()); + } else { + tester.remove(queue, random()); + } + + return checkAndSize(queue); + }); + + if (ops++ >= size && random().nextInt(4) == 0) { + ops -= size; + + counter.test(() -> { + queues.addAll(tester.linearTest(queue, random())); + checkAndSize(queue); + }); + } + } + + for (final M queue : queues) { + counter.test(() -> { + tester.linearTest(queue, random()); + checkAndSize(queue); + for (int i = queue.size(); i > 0; i--) { + tester.remove(queue, random()); + checkAndSize(queue); + } + }); + } + }); + } + + private ExtendedRandom random() { + return counter.random(); + } + } +} diff --git a/java/queue/LinkedQueue.java b/java/queue/LinkedQueue.java new file mode 100644 index 0000000..ee18cda --- /dev/null +++ b/java/queue/LinkedQueue.java @@ -0,0 +1,114 @@ +package queue; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class LinkedQueue extends AbstractQueue { + + private Node head; + private Node tail; + + private static class Node { + + Object value; + Node next; + Node prev; + + Node(Object value) { + this.value = value; + } + } + + public LinkedQueue() { + head = null; + tail = null; + } + + @Override + public void enqueue(Object element) { + assert element != null : "Element must not be null"; + Node node = new Node(element); + if (tail == null) { + head = tail = node; + } else { + node.prev = tail; + tail.next = node; + tail = node; + } + size++; + } + + @Override + public void push(Object element) { + assert element != null : "Element must not be null"; + Node node = new Node(element); + if (head == null) { + head = tail = node; + } else { + node.next = head; + head.prev = node; + head = node; + } + size++; + } + + @Override + public Object element() { + assert size > 0 : "Queue is empty"; + return head.value; + } + + @Override + public Object peek() { + assert size > 0 : "Queue is empty"; + return tail.value; + } + + @Override + public Object dequeue() { + assert size > 0 : "Queue is empty"; + Object result = head.value; + head = head.next; + if (head != null) { + head.prev = null; + } else { + tail = null; + } + size--; + return result; + } + + @Override + public Object remove() { + assert size > 0 : "Queue is empty"; + Object result = tail.value; + tail = tail.prev; + if (tail != null) { + tail.next = null; + } else { + head = null; + } + size--; + return result; + } + + @Override + protected Object get(int i) { + Node cur = head; + for (int j = 0; j < i; j++) { + cur = cur.next; + } + return cur.value; + } + + @Override + protected void clearImpl() { + head = null; + tail = null; + } + + @Override + protected AbstractQueue createEmpty() { + return new LinkedQueue(); + } +} diff --git a/java/queue/Queue.java b/java/queue/Queue.java new file mode 100644 index 0000000..cd2e48d --- /dev/null +++ b/java/queue/Queue.java @@ -0,0 +1,56 @@ +package queue; + +import java.util.function.Predicate; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public interface Queue { + void enqueue(Object element); + + void push(Object element); + + Object element(); + + Object peek(); + + Object dequeue(); + + Object remove(); + + int size(); + + boolean isEmpty(); + + void clear(); + + int count(Object element); + + int countIf(Predicate predicate); + + int indexIf(Predicate predicate); + + int lastIndexIf(Predicate predicate); + + int indexOf(Object element); + + int lastIndexOf(Object element); + + boolean contains(Object element); + + boolean removeFirst(Object element); + + Queue getNth(int n); + + Queue removeNth(int n); + + void dropNth(int n); + + void removeIf(java.util.function.Predicate predicate); + + void retainIf(java.util.function.Predicate predicate); + + void removeAll(Object element); + + void retainAll(Object element); +} diff --git a/java/queue/QueueTest.java b/java/queue/QueueTest.java new file mode 100644 index 0000000..6692657 --- /dev/null +++ b/java/queue/QueueTest.java @@ -0,0 +1,51 @@ +package queue; + +import static queue.Queues.*; + +import base.Selector; +import base.TestCounter; +import java.util.List; +import java.util.function.Consumer; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class QueueTest { + + public static final Selector SELECTOR = new Selector(QueueTest.class) + .variant("Base", variant(QueueModel.class, d -> () -> d)) + .variant("3637", variant(ContainsModel.class, d -> () -> d, CONTAINS)) + .variant("3839", variant(NthModel.class, d -> () -> d, NTH)) + .variant( + "3435", + variant(RemoveIfModel.class, d -> () -> d, REMOVE_IF_TEST) + ) + .variant( + "3233", + variant(RemoveEqModel.class, d -> () -> d, REMOVE_EQ_TEST) + ); + + private QueueTest() {} + + public static void main(final String... args) { + SELECTOR.main(args); + } + + /* package-private */ static < + M extends QueueModel, + T extends QueueChecker + > Consumer variant( + final Class type, + final T tester, + final Queues.Splitter splitter + ) { + return new QueueTester<>(type, tester, splitter)::test; + } + + /* package-private */ static < + M extends QueueModel, + T extends QueueChecker + > Consumer variant(final Class type, final T tester) { + return variant(type, tester, (t, q, r) -> List.of()); + } +} diff --git a/java/queue/QueueTester.java b/java/queue/QueueTester.java new file mode 100644 index 0000000..32b50c9 --- /dev/null +++ b/java/queue/QueueTester.java @@ -0,0 +1,32 @@ +package queue; + +import base.Asserts; +import base.TestCounter; + +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class QueueTester extends ArrayQueueTester { + /* package-private */ QueueTester(final Class type, final Queues.QueueChecker tester, final Queues.Splitter splitter) { + super(type, tester, splitter); + } + + public void test(final TestCounter counter) { + test(counter, "LinkedQueue", ReflectionTest.Mode.CLASS); + test(counter, "ArrayQueue", ReflectionTest.Mode.CLASS); + } + + private static boolean implementsQueue(final Class type) { + return type != Object.class + && (Stream.of(type.getInterfaces()).map(Class::getName).anyMatch("queue.Queue"::equals) + || implementsQueue(type.getSuperclass())); + } + + @Override + protected void checkImplementation(final Class type) { + Asserts.assertTrue(type + " should extend AbstractQueue", "queue.AbstractQueue".equals(type.getSuperclass().getName())); + Asserts.assertTrue(type + " should implement interface Queue", implementsQueue(type)); + } +} diff --git a/java/queue/Queues.java b/java/queue/Queues.java new file mode 100644 index 0000000..7237ce5 --- /dev/null +++ b/java/queue/Queues.java @@ -0,0 +1,490 @@ +package queue; + +import base.ExtendedRandom; +import java.lang.reflect.Field; +import java.util.ArrayDeque; +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("UnusedReturnValue") +public final class Queues { + + private Queues() {} + + protected interface QueueModel { + @ReflectionTest.Ignore + ArrayDeque model(); + + default Object dequeue() { + return model().removeFirst(); + } + + default int size() { + return model().size(); + } + + default boolean isEmpty() { + return model().isEmpty(); + } + + default void clear() { + model().clear(); + } + + default void enqueue(final Object element) { + model().addLast(element); + } + + default Object element() { + return model().getFirst(); + } + } + + protected interface QueueChecker { + M wrap(ArrayDeque reference); + + default List linearTest(final M queue, final ExtendedRandom random) { + // Do nothing by default + return List.of(); + } + + default void check(final M queue, final ExtendedRandom random) { + queue.element(); + } + + default void add( + final M queue, + final Object element, + final ExtendedRandom random + ) { + queue.enqueue(element); + } + + default Object randomElement(final ExtendedRandom random) { + return ArrayQueueTester + .ELEMENTS[random.nextInt(ArrayQueueTester.ELEMENTS.length)]; + } + + default void remove(final M queue, final ExtendedRandom random) { + queue.dequeue(); + } + + @SuppressWarnings("unchecked") + default M cast(final QueueModel model) { + return (M) model; + } + } + + @FunctionalInterface + protected interface Splitter { + List split( + final QueueChecker tester, + final M queue, + final ExtendedRandom random + ); + } + + @FunctionalInterface + /* package-private */ interface LinearTester< + M extends QueueModel + > extends Splitter { + void test( + final QueueChecker tester, + final M queue, + final ExtendedRandom random + ); + + @Override + default List split( + final QueueChecker tester, + final M queue, + final ExtendedRandom random + ) { + test(tester, queue, random); + return List.of(); + } + } + + // === 3637 (ArrayQueue) + + /* package-private */ interface CountModel extends ReflectionModel { + default int count(final Object element) { + return reduce(0, element, (v, i) -> v + 1); + } + } + + /* package-private */ static final Queues.LinearTester COUNT = ( + tester, + queue, + random + ) -> queue.count(tester.randomElement(random)); + + /* package-private */ interface DequeCountModel + extends DequeModel, CountModel {} + + /* package-private */ static final Queues.LinearTester< + DequeCountModel + > DEQUE_COUNT = COUNT::test; + + // === 3839 (ArrayQueue) + + /* package-private */ interface CountIfModel extends ReflectionModel { + default int countIf(final Predicate p) { + return reduce(0, p, (v, i) -> v + 1); + } + } + + /* package-private */ static final Queues.LinearTester< + CountIfModel + > COUNT_IF = (t, q, r) -> q.countIf(randomPredicate(t, r)); + + /* package-private */ interface DequeCountIfModel + extends DequeModel, CountIfModel {} + + /* package-private */ static final Queues.LinearTester< + DequeCountIfModel + > DEQUE_COUNT_IF = COUNT_IF::test; + + record NamedPredicate( + String name, + Predicate predicate + ) implements Predicate { + @Override + public String toString() { + return name; + } + + @Override + public boolean test(final T t) { + return predicate.test(t); + } + } + + /* package-private */ static Predicate randomPredicate( + final Queues.QueueChecker tester, + final ExtendedRandom random + ) { + final Object item = tester.randomElement(random); + return random + .>>randomItem( + () -> new NamedPredicate<>("item == ", o -> item == o), + () -> new NamedPredicate<>(item + ".equals", item::equals), + () -> new NamedPredicate<>("null == ", Objects::isNull) + ) + .get(); + } + + @SafeVarargs + private static Queues.LinearTester< + M + > randomOf(final Queues.LinearTester... variants) { + return (tester, queue, random) -> + random.randomItem(variants).test(tester, queue, random); + } + + // === 3435 (ArrayQueue) + + /* package-private */ + interface IndexIfModel extends ReflectionModel { + default int indexIf(final Predicate p) { + return reduce(-1, p, (v, i) -> v == -1 ? i : v); + } + + default int lastIndexIf(final Predicate p) { + return reduce(-1, p, (v, i) -> i); + } + } + + /* package-private */ static final Queues.LinearTester< + IndexIfModel + > INDEX_IF = randomOf( + (t, q, r) -> q.indexIf(randomPredicate(t, r)), + (t, q, r) -> q.lastIndexIf(randomPredicate(t, r)) + ); + + // === 3233 (ArrayQueue) + + /* package-private */ + interface IndexModel extends ReflectionModel { + default int indexOf(final Object element) { + return reduce(-1, element, (v, i) -> v == -1 ? i : v); + } + + default int lastIndexOf(final Object element) { + return reduce(-1, element, (v, i) -> i); + } + } + + /* package-private */ static final Queues.LinearTester INDEX = + randomOf( + (t, q, r) -> q.indexOf(t.randomElement(r)), + (t, q, r) -> q.lastIndexOf(t.randomElement(r)) + ); + + // === 3637 (Queues) + + /* package-private */ interface ContainsModel extends Queues.QueueModel { + default boolean contains(final Object element) { + return model().contains(element); + } + + @SuppressWarnings("UnusedReturnValue") + default boolean removeFirst(final Object element) { + return model().removeFirstOccurrence(element); + } + } + + /* package-private */ static final Queues.LinearTester< + ContainsModel + > CONTAINS = (tester, queue, random) -> { + final Object element = random.nextBoolean() + ? tester.randomElement(random) + : random.nextInt(); + if (random.nextBoolean()) { + queue.contains(element); + } else { + queue.removeFirst(element); + } + }; + + // === 3839 (Queues) + + /* package-private */ interface NthModel extends Queues.QueueModel { + // Deliberately ugly implementation + @ReflectionTest.Wrap + default NthModel getNth(final int n) { + final ArrayDeque deque = new ArrayDeque<>(); + final int[] index = { 0 }; + model().forEach(e -> { + if (++index[0] % n == 0) { + deque.add(e); + } + }); + return () -> deque; + } + + // Deliberately ugly implementation + @ReflectionTest.Wrap + default NthModel removeNth(final int n) { + final ArrayDeque deque = new ArrayDeque<>(); + final int[] index = { 0 }; + model().removeIf(e -> { + if (++index[0] % n == 0) { + deque.add(e); + return true; + } else { + return false; + } + }); + return () -> deque; + } + + default void dropNth(final int n) { + final int[] index = { 0 }; + model().removeIf(e -> ++index[0] % n == 0); + } + } + + /* package-private */ static final Queues.Splitter NTH = ( + tester, + queue, + random + ) -> { + final int n = random.nextInt(5) + 1; + return switch (random.nextInt(3)) { + case 0 -> { + final NthModel model = queue.removeNth(n); + yield List.of(tester.cast(model)); + } + case 1 -> { + queue.dropNth(n); + yield List.of(); + } + case 2 -> List.of(tester.cast(queue.getNth(n))); + default -> throw new AssertionError(); + }; + }; + + // === 3435 (Queues) + + /* package-private */ interface RemoveIfModel extends Queues.QueueModel { + default void removeIf(final Predicate p) { + model().removeIf(p); + } + + default void retainIf(final Predicate p) { + model().removeIf(Predicate.not(p)); + } + } + + private static final Queues.LinearTester REMOVE_IF = ( + tester, + queue, + random + ) -> queue.removeIf(randomPredicate(tester, random)); + private static final Queues.LinearTester RETAIN_IF = ( + tester, + queue, + random + ) -> queue.retainIf(randomPredicate(tester, random)); + /* package-private */ static final Queues.LinearTester< + RemoveIfModel + > REMOVE_IF_TEST = randomOf(REMOVE_IF, RETAIN_IF); + + // 3233 (Queues) + + /* package-private */ interface RemoveEqModel extends Queues.QueueModel { + default void removeAll(final Object element) { + model().removeIf(Predicate.isEqual(element)); + } + + default void retainAll(final Object element) { + model().removeIf(Predicate.not(Predicate.isEqual(element))); + } + } + + private static final Queues.LinearTester REMOVE_ALL = ( + tester, + queue, + random + ) -> queue.removeAll(tester.randomElement(random)); + private static final Queues.LinearTester RETAIL_ALL = ( + tester, + queue, + random + ) -> queue.retainAll(tester.randomElement(random)); + /* package-private */ static final Queues.LinearTester< + RemoveEqModel + > REMOVE_EQ_TEST = randomOf(REMOVE_ALL, RETAIL_ALL); + + // === Reflection + + /* package-private */ interface ReflectionModel extends QueueModel { + Field ELEMENTS = getField("elements"); + Field HEAD = getField("head"); + + @SuppressWarnings("unchecked") + private Z get(final Field field) { + try { + return (Z) field.get(model()); + } catch (final IllegalAccessException e) { + throw new AssertionError( + "Cannot access field " + + field.getName() + + ": " + + e.getMessage(), + e + ); + } + } + + private static Field getField(final String name) { + try { + final Field field = ArrayDeque.class.getDeclaredField(name); + field.setAccessible(true); + return field; + } catch (final NoSuchFieldException e) { + throw new AssertionError( + "Reflection error: " + e.getMessage(), + e + ); + } + } + + @ReflectionTest.Ignore + default int head() { + return get(HEAD); + } + + @ReflectionTest.Ignore + default Object[] elements() { + return get(ELEMENTS); + } + + @ReflectionTest.Ignore + default R reduce( + final R zero, + final Predicate p, + final BiFunction f + ) { + final int size = size(); + final Object[] elements = elements(); + final int head = head(); + R result = zero; + for (int i = 0; i < size; i++) { + if (p.test(elements[(head + i) % elements.length])) { + result = f.apply(result, i); + } + } + return result; + } + + @ReflectionTest.Ignore + @SuppressWarnings("unused") + default R reduce( + final R zero, + final Object v, + final BiFunction f + ) { + return reduce(zero, o -> Objects.equals(v, o), f); + } + } + + // === Deque + + /* package-private */ interface DequeModel extends QueueModel { + default void push(final Object element) { + model().addFirst(element); + } + + @SuppressWarnings("UnusedReturnValue") + default Object peek() { + return model().getLast(); + } + + default Object remove() { + return model().removeLast(); + } + } + + /* package-private */ interface DequeChecker< + M extends DequeModel + > extends QueueChecker { + @Override + default void add( + final M queue, + final Object element, + final ExtendedRandom random + ) { + if (random.nextBoolean()) { + QueueChecker.super.add(queue, element, random); + } else { + queue.push(element); + } + } + + @Override + default void check(final M queue, final ExtendedRandom random) { + if (random.nextBoolean()) { + QueueChecker.super.check(queue, random); + } else { + queue.peek(); + } + } + + @Override + default void remove(final M queue, final ExtendedRandom random) { + if (random.nextBoolean()) { + QueueChecker.super.remove(queue, random); + } else { + queue.remove(); + } + } + } +} diff --git a/java/queue/ReflectionTest.java b/java/queue/ReflectionTest.java new file mode 100644 index 0000000..a4e2d32 --- /dev/null +++ b/java/queue/ReflectionTest.java @@ -0,0 +1,201 @@ +package queue; + +import base.Asserts; +import base.TestCounter; + +import java.lang.annotation.*; +import java.lang.reflect.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ReflectionTest { + private static final boolean DEBUG = false; + public static final Collector JOINER = Collectors.joining(", ", "(", ")"); + + protected final Set methods = new HashSet<>(); + + public ReflectionTest() { + Asserts.checkAssert(getClass()); + } + + protected void checkResult(final String call, final Object expected, final Object actual) { + Asserts.assertEquals(call, expected, actual); + } + + protected T checking(final TestCounter counter, final Class type, final T reference, final T tested) { + return proxy(type, (proxy, method, args) -> { + final String call = method.getName() + (args == null ? "()" : args(args)); + if (DEBUG) { + counter.format("\t%s%n", call); + } + methods.add(method); + final Object expected; + final Object actual; + try { + expected = method.invoke(reference, args); + actual = method.invoke(tested, args); + } catch (final InvocationTargetException e) { + throw e.getCause(); + } + checkResult(call, expected, actual); + return actual; + }); + } + + protected static String args(final Object[] args) { + return Stream.of(args).map(Objects::toString).collect(JOINER); + } + + protected static T proxy(final Class type, final InvocationHandler handler) { + return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, handler)); + } + + + private interface IMethod { + Object invoke(final Object instance, final Object[] args) throws InvocationTargetException, IllegalAccessException; + } + + + protected enum Mode { + MODULE("Module") { + @Override + IMethod lookupMethod(final Class type, final Method method) { + return findMethod(true, type, method); + } + }, + ADT("ADT") { + @Override + IMethod lookupMethod(final Class type, final Method method) { + final Class[] argTypes = Stream.concat(Stream.of(type), Stream.of(method.getParameterTypes())) + .toArray(Class[]::new); + final Method actual = findMethod(true, type, method.getName(), argTypes); + + final Object[] a = new Object[method.getParameterTypes().length + 1]; + return (instance, args) -> { + a[0] = instance; + if (args != null) { + System.arraycopy(args, 0, a, 1, args.length); + } + return actual.invoke(null, a); + }; + } + }, + CLASS("") { + @Override + IMethod lookupMethod(final Class type, final Method method) { + return findMethod(false, type, method); + } + }; + + private static IMethod findMethod(final boolean isStatic, final Class type, final Method method) { + return findMethod(isStatic, type, method.getName(), method.getParameterTypes())::invoke; + } + + private static Method findMethod(final boolean isStatic, final Class type, final String name, final Class[] parameterTypes) { + final String description = name + args(parameterTypes); + final Method method; + try { + method = type.getMethod(name, parameterTypes); + } catch (final NoSuchMethodException e) { + throw Asserts.error("Missing method %s in %s", description, type); + } + if (isStatic != Modifier.isStatic(method.getModifiers())) { + throw Asserts.error("Method %s in %s %s be static", description, type, isStatic ? "must" : "must not"); + } + return method; + } + + private final String suffix; + + Mode(final String suffix) { + this.suffix = suffix; + } + + abstract IMethod lookupMethod(final Class type, final Method method); + + private Class loadClass(final String baseName) { + final String className = baseName + suffix; + try { + final URL url = Paths.get(".").toUri().toURL(); + //noinspection resource + return new URLClassLoader(new URL[]{url}).loadClass(className); + } catch (final MalformedURLException e) { + throw new AssertionError("Cannot load classes from .", e); + } catch (final ClassNotFoundException e) { + throw new AssertionError("Cannot find class %s: %s".formatted(className, e.getMessage()), e); + } + } + } + + protected static class ProxyFactory { + private final Class type; + private final Map methods; + private final Constructor constructor; + protected final Class implementation; + + protected ProxyFactory(final Class type, final Mode mode, final String baseName) { + implementation = mode.loadClass(baseName); + try { + constructor = implementation.getConstructor(); + } catch (final NoSuchMethodException e) { + throw Asserts.error("%s should have default constructor", implementation.getName()); + } + + this.type = type; + methods = Stream.of(type.getMethods()) + .filter(method -> !method.isAnnotationPresent(Ignore.class)) + .collect(Collectors.toMap(Function.identity(), method -> mode.lookupMethod(implementation, method))); + } + + public T create() { + try { + return wrap(constructor.newInstance()); + } catch (final InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new AssertionError("Cannot create " + implementation.getName() + ": " + e.getMessage(), e); + } + } + + private T wrap(final Object instance) { + Asserts.assertEquals("same class", implementation, instance.getClass()); + + return proxy(type, (proxy, method, args) -> { + if (method.getName().equals("toString")) { + return instance.toString(); + } + final Object result; + try { + result = methods.get(method).invoke(instance, args); + } catch (final InvocationTargetException e) { + throw e.getCause(); + } + if (method.isAnnotationPresent(Wrap.class)) { + return wrap(result); + } + return result; + }); + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Inherited + protected @interface Ignore {} + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Inherited + protected @interface Wrap {} +} diff --git a/java/queue/package-info.java b/java/queue/package-info.java new file mode 100644 index 0000000..ed57a85 --- /dev/null +++ b/java/queue/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Queue homeworks + * of Paradigms of Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package queue; \ No newline at end of file diff --git a/java/search/BinarySearch.java b/java/search/BinarySearch.java new file mode 100644 index 0000000..4cb9d52 --- /dev/null +++ b/java/search/BinarySearch.java @@ -0,0 +1,65 @@ +package search; + +/** + * @author Nikita Doschennikov (me@fymio.us) + */ +public class BinarySearch { + + public static void main(String[] args) { + IntList a = new IntList(); + int x = Integer.parseInt(args[0]); + + int n = args.length; + + for (int i = 1; i < n; i++) { + a.put(Integer.parseInt(args[i])); + } + + System.out.println(searchRecursive(x, a)); + // System.out.println(searchIterative(x, a)); + } + + static int searchIterative(int x, IntList a) { + if (a.getLength() == 0) { + return 0; + } + + int low = 0, + high = a.getLength() - 1; + + while (low <= high) { + int mid = low + (high - low) / 2; + + if (a.get(mid) <= x) { + high = mid - 1; + } else { + low = mid + 1; + } + } + + return low; + } + + static int searchRecursive(int x, IntList a) { + return searchRecursiveHelper(x, a, 0, a.getLength() - 1); + } + + private static int searchRecursiveHelper( + int x, + IntList a, + int low, + int high + ) { + if (low > high) { + return low; + } + + int mid = low + (high - low) / 2; + + if (a.get(mid) <= x) { + return searchRecursiveHelper(x, a, low, mid - 1); + } else { + return searchRecursiveHelper(x, a, mid + 1, high); + } + } +} diff --git a/java/search/BinarySearch3233.java b/java/search/BinarySearch3233.java new file mode 100644 index 0000000..43fb2f2 --- /dev/null +++ b/java/search/BinarySearch3233.java @@ -0,0 +1,50 @@ +package search; + +/** + * @author Nikita Doschennikov (me@fymio.us) + */ +public class BinarySearch3233 { + + public static void main(String[] args) { + IntList a = new IntList(); + for (String arg : args) { + a.put(Integer.parseInt(arg)); + } + + // System.out.println(searchIterative(a)); + System.out.println(searchRecursive(a, 0, a.getLength() - 1)); + } + + static int searchIterative(IntList a) { + int arrLength = a.getLength(); + if (arrLength <= 1) return 0; + + int low = 0, high = arrLength - 1; + + while (low < high) { + int mid = low + (high - low) / 2; + if (a.get(mid) < a.get(arrLength - 1)) { + high = mid; + } else { + low = mid + 1; + } + } + + return low; + } + + static int searchRecursive(IntList a, int low, int high) { + if (low >= high) { + return low; + } + + int mid = low + (high - low) / 2; + if (a.get(mid) < a.get(a.getLength() - 1)) { + return searchRecursive(a, low, mid); + } else { + return searchRecursive(a, mid + 1, high); + } + } + + +} \ No newline at end of file diff --git a/java/search/BinarySearch3435.java b/java/search/BinarySearch3435.java new file mode 100644 index 0000000..c5dfca2 --- /dev/null +++ b/java/search/BinarySearch3435.java @@ -0,0 +1,48 @@ +package search; + +/** + * @author Nikita Doschennikov (me@fymio.us) + */ +public class BinarySearch3435 { + + public static void main(String[] args) { + IntList a = new IntList(); + for (String arg : args) { + a.put(Integer.parseInt(arg)); + } + // System.out.println(searchIterative(a)); + System.out.println(searchRecursive(a, 0, a.getLength() - 1)); + } + + static int searchIterative(IntList a) { + int arrLength = a.getLength(); + if (arrLength <= 1) return 0; + + int low = 0, high = arrLength - 1; + + while (low < high) { + int mid = low + (high - low) / 2; + if (a.get(mid) > a.get(arrLength - 1)) { + high = mid; + } else { + low = mid + 1; + } + } + + return low; + } + + static int searchRecursive(IntList a, int low, int high) { + if (low >= high) { + return low; + } + + int mid = low + (high - low) / 2; + if (a.get(mid) > a.get(a.getLength() - 1)) { + return searchRecursive(a, low, mid); + } else { + return searchRecursive(a, mid + 1, high); + } + } + +} \ No newline at end of file diff --git a/java/search/BinarySearch3637.java b/java/search/BinarySearch3637.java new file mode 100644 index 0000000..19e4633 --- /dev/null +++ b/java/search/BinarySearch3637.java @@ -0,0 +1,77 @@ +package search; + +/** + * @author Nikita Doschennikov (me@fymio.us) + */ +public class BinarySearch3637 { + + public static void main(String[] args) { + IntList a = new IntList(); + int x = Integer.parseInt(args[0]); + int n = args.length; + for (int i = 1; i < n; i++) { + a.put(Integer.parseInt(args[i])); + } + // System.out.println(rightBoundIterative(x, a) - leftBoundIterative(x, a)); + System.out.println( + rightBoundRecursive(x, a, 0, a.getLength() - 1) - + leftBoundRecursive(x, a, 0, a.getLength() - 1) + ); + } + + static int leftBoundIterative(int x, IntList a) { + int low = 0, + high = a.getLength() - 1; + while (low <= high) { + int mid = low + (high - low) / 2; + if (a.get(mid) > x) { + low = mid + 1; + } else { + high = mid - 1; + } + } + return high + 1; + } + + static int rightBoundIterative(int x, IntList a) { + int low = 0, + high = a.getLength() - 1; + while (low <= high) { + int mid = low + (high - low) / 2; + if (a.get(mid) >= x) { + low = mid + 1; + } else { + high = mid - 1; + } + } + return high + 1; + } + + static int leftBoundRecursive(int x, IntList a, int low, int high) { + if (low > high) { + return high + 1; + } + + int mid = low + (high - low) / 2; + + if (a.get(mid) > x) { + return leftBoundRecursive(x, a, mid + 1, high); + } else { + return leftBoundRecursive(x, a, low, mid - 1); + } + } + + static int rightBoundRecursive(int x, IntList a, int low, int high) { + if (low > high) { + return high + 1; + } + + int mid = low + (high - low) / 2; + + if (a.get(mid) >= x) { + return rightBoundRecursive(x, a, mid + 1, high); + } else { + return rightBoundRecursive(x, a, low, mid - 1); + } + } +} diff --git a/java/search/BinarySearch3839.java b/java/search/BinarySearch3839.java new file mode 100644 index 0000000..269ca0f --- /dev/null +++ b/java/search/BinarySearch3839.java @@ -0,0 +1,77 @@ +package search; + +/** + * @author Nikita Doschennikov (me@fymio.us) + */ +public class BinarySearch3839 { + + public static void main(String[] args) { + IntList a = new IntList(); + int x = Integer.parseInt(args[0]); + int n = args.length; + for (int i = 1; i < n; i++) { + a.put(Integer.parseInt(args[i])); + } + + int leftBound = leftBoundIterative(x, a); + int rightBound = rightBoundIterative(x, a); + int range = rightBound - leftBound; + System.out.println(leftBound + " " + range); + } + + static int leftBoundIterative(int x, IntList a) { + int low = 0, + high = a.getLength() - 1; + while (low <= high) { + int mid = low + (high - low) / 2; + if (a.get(mid) > x) { + low = mid + 1; + } else { + high = mid - 1; + } + } + return high + 1; + } + + static int rightBoundIterative(int x, IntList a) { + int low = 0, + high = a.getLength() - 1; + while (low <= high) { + int mid = low + (high - low) / 2; + if (a.get(mid) >= x) { + low = mid + 1; + } else { + high = mid - 1; + } + } + return high + 1; + } + + static int leftBoundRecursive(int x, IntList a, int low, int high) { + if (low > high) { + return high + 1; + } + + int mid = low + (high - low) / 2; + + if (a.get(mid) > x) { + return leftBoundRecursive(x, a, mid + 1, high); + } else { + return leftBoundRecursive(x, a, low, mid - 1); + } + } + + static int rightBoundRecursive(int x, IntList a, int low, int high) { + if (low > high) { + return high + 1; + } + + int mid = low + (high - low) / 2; + + if (a.get(mid) >= x) { + return rightBoundRecursive(x, a, mid + 1, high); + } else { + return rightBoundRecursive(x, a, low, mid - 1); + } + } +} diff --git a/java/search/BinarySearchTest.java b/java/search/BinarySearchTest.java new file mode 100644 index 0000000..0859b50 --- /dev/null +++ b/java/search/BinarySearchTest.java @@ -0,0 +1,180 @@ +package search; + +import base.MainChecker; +import base.Runner; +import base.Selector; +import base.TestCounter; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.util.stream.IntStream.range; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class BinarySearchTest { + public static final int[] SIZES = {5, 4, 2, 1, 10, 50, 100, 200, 300}; + public static final int[] VALUES = new int[]{5, 4, 2, 1, 0, 10, 100, Integer.MAX_VALUE / 2}; + + private BinarySearchTest() { + } + + // === Base + /* package-private */ static int base(final int c, final int x, final int[] a) { + return IntStream.range(0, a.length).filter(i -> Integer.compare(a[i], x) != c).findFirst().orElse(a.length); + } + + + // === 3637 + + private static int count(final int c, final int x, final int[] a) { + return (int) range(0, a.length).filter(i -> a[i] == x).count(); + } + + + // === 3839 + + private static String span(final int c, final int x, final int[] a) { + final int begin = range(0, a.length).filter(i -> a[i] == x || Integer.compare(x, a[i]) == c).findFirst().orElse(a.length); + final long length = range(0, a.length).filter(i -> a[i] == x).count(); + return begin + " " + length; + } + + + // === 3435 + + private static Consumer shift(final String name, final Kind kind) { + final Sampler sampler = new Sampler(kind, false, false); + return variant(name, variant -> { + for (final int s : SIZES) { + final int size = s > 3 * TestCounter.DENOMINATOR ? s / TestCounter.DENOMINATOR : s; + for (final int max : VALUES) { + final int[] a = sampler.sample(variant, size, max); + for (int k = 0; k < a.length; k++) { + variant.test(k, a); + + final int last = a[a.length - 1]; + System.arraycopy(a, 0, a, 1, a.length - 1); + a[0] = last; + } + } + } + }); + } + + // === Common code + + public static final Selector SELECTOR = new Selector(BinarySearchTest.class) + .variant("Base", Solver.variant0("", Kind.DESC, BinarySearchTest::base)) + .variant("3637", Solver.variant0("3637", Kind.DESC, BinarySearchTest::count)) + .variant("3839", Solver.variant0("3839", Kind.DESC, BinarySearchTest::span)) + .variant("3435", shift("3435", Kind.DESC)) + .variant("3233", shift("3233", Kind.ASC)) + ; + + public static void main(final String... args) { + SELECTOR.main(args); + } + + /* package-private */ static Consumer variant(final String name, final Consumer variant) { + final String className = "BinarySearch" + name; + return counter -> variant.accept(new Variant(counter, new MainChecker(Runner.packages("search").args(className)))); + } + + /* package-private */ interface Solver { + static Consumer variant0(final String name, final Kind kind, final Solver solver) { + return variant(name, kind, true, solver); + } + + static Consumer variant1(final String name, final Kind kind, final Solver solver) { + return variant(name, kind, false, solver); + } + + private static Consumer variant(final String name, final Kind kind, final boolean empty, final Solver solver) { + final Sampler sampler = new Sampler(kind, true, true); + return BinarySearchTest.variant(name, vrt -> { + if (empty) { + solver.test(kind, vrt); + } + solver.test(kind, vrt, 0); + for (final int s1 : SIZES) { + final int size = s1 > 3 * TestCounter.DENOMINATOR ? s1 / TestCounter.DENOMINATOR : s1; + for (final int max : VALUES) { + solver.test(kind, vrt, sampler.sample(vrt, size, max)); + } + } + }); + } + + private static int[] probes(final int[] a, final int limit) { + return Stream.of( + Arrays.stream(a), + IntStream.range(1, a.length).map(i -> (a[i - 1] + a[i]) / 2), + IntStream.of( + 0, Integer.MIN_VALUE, Integer.MAX_VALUE, + a.length > 0 ? a[0] - 1 : -1, + a.length > 0 ? a[a.length - 1] + 1 : 1 + ) + ) + .flatMapToInt(Function.identity()) + .distinct() + .sorted() + .limit(limit) + .toArray(); + } + + Object solve(final int c, final int x, final int... a); + + default void test(final Kind kind, final Variant variant, final int... a) { + test(kind, variant, a, Integer.MAX_VALUE); + } + + default void test(final Kind kind, final Variant variant, final int[] a, final int limit) { + for (final int x : probes(a, limit)) { + variant.test(solve(kind.d, x, a), IntStream.concat(IntStream.of(x), Arrays.stream(a))); + } + } + } + + public enum Kind { + ASC(-1), DESC(1); + + public final int d; + + Kind(final int d) { + this.d = d; + } + } + + public record Variant(TestCounter counter, MainChecker checker) { + void test(final Object expected, final IntStream ints) { + final List input = ints.mapToObj(Integer::toString).toList(); + checker.testEquals(counter, input, List.of(expected.toString())); + } + + public void test(final Object expected, final int[] a) { + test(expected, Arrays.stream(a)); + } + } + + public record Sampler(Kind kind, boolean dups, boolean zero) { + public int[] sample(final Variant variant, final int size, final int max) { + final IntStream sorted = variant.counter.random().getRandom().ints(zero ? size : Math.max(size, 1), -max, max + 1).sorted(); + final int[] ints = (dups ? sorted : sorted.distinct()).toArray(); + if (kind == Kind.DESC) { + final int sz = ints.length; + for (int i = 0; i < sz / 2; i++) { + final int t = ints[i]; + ints[i] = ints[sz - i - 1]; + ints[sz - i - 1] = t; + } + } + return ints; + } + } +} diff --git a/java/search/IntList.java b/java/search/IntList.java new file mode 100644 index 0000000..ad312df --- /dev/null +++ b/java/search/IntList.java @@ -0,0 +1,48 @@ +package search; + +import java.util.Arrays; + +/** + * @author Nikita Doschennikov (me@fymio.us) + */ +public class IntList { + + protected int[] list = new int[8]; + protected int idx = 0; + + public IntList() {} + + public IntList(int[] list) { + this.list = list; + } + + public void put(int val) { + if (idx >= list.length) { + list = Arrays.copyOf(list, list.length * 2); + } + + list[idx++] = val; + } + + public int getLength() { + return idx; + } + + public int get(int index) { + return list[index]; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < idx; i++) { + if (i == idx - 1) { + sb.append(String.valueOf(list[i]) + "\n"); + } else { + sb.append(String.valueOf(list[i]) + " "); + } + } + + return sb.toString(); + } +} diff --git a/java/search/package-info.java b/java/search/package-info.java new file mode 100644 index 0000000..286d139 --- /dev/null +++ b/java/search/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Binary Search homework + * of Paradigms of Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package search; \ No newline at end of file diff --git a/javascript/.gitattributes b/javascript/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/javascript/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/javascript/RunJS.cmd b/javascript/RunJS.cmd new file mode 100644 index 0000000..641a3e9 --- /dev/null +++ b/javascript/RunJS.cmd @@ -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" diff --git a/javascript/RunJS.html b/javascript/RunJS.html new file mode 100644 index 0000000..dc37047 --- /dev/null +++ b/javascript/RunJS.html @@ -0,0 +1,47 @@ + + +

+
+
diff --git a/javascript/RunJS.java b/javascript/RunJS.java
new file mode 100644
index 0000000..839a73c
--- /dev/null
+++ b/javascript/RunJS.java
@@ -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 + "')");
+    }
+}
diff --git a/javascript/RunJS.node.js b/javascript/RunJS.node.js
new file mode 100644
index 0000000..fcfda1f
--- /dev/null
+++ b/javascript/RunJS.node.js
@@ -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());
diff --git a/javascript/RunJS.sh b/javascript/RunJS.sh
new file mode 100644
index 0000000..fb6ff0c
--- /dev/null
+++ b/javascript/RunJS.sh
@@ -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 $@
diff --git a/javascript/TestJS.cmd b/javascript/TestJS.cmd
new file mode 100644
index 0000000..4e818f6
--- /dev/null
+++ b/javascript/TestJS.cmd
@@ -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%
diff --git a/javascript/TestJS.sh b/javascript/TestJS.sh
new file mode 100755
index 0000000..27ded0a
--- /dev/null
+++ b/javascript/TestJS.sh
@@ -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
diff --git a/javascript/example.js b/javascript/example.js
new file mode 100644
index 0000000..e947f1f
--- /dev/null
+++ b/javascript/example.js
@@ -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+)|(?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
+  );
+}
diff --git a/javascript/examples.js b/javascript/examples.js
new file mode 100644
index 0000000..f2d53c2
--- /dev/null
+++ b/javascript/examples.js
@@ -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");
diff --git a/javascript/examples/0_1_magic.js b/javascript/examples/0_1_magic.js
new file mode 100644
index 0000000..b3e13a9
--- /dev/null
+++ b/javascript/examples/0_1_magic.js
@@ -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;
+    }
+}
diff --git a/javascript/examples/1_1_types.js b/javascript/examples/1_1_types.js
new file mode 100644
index 0000000..479f674
--- /dev/null
+++ b/javascript/examples/1_1_types.js
@@ -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!'");
diff --git a/javascript/examples/1_2_arrays.js b/javascript/examples/1_2_arrays.js
new file mode 100644
index 0000000..7309642
--- /dev/null
+++ b/javascript/examples/1_2_arrays.js
@@ -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");
+}
diff --git a/javascript/examples/1_3_functions.js b/javascript/examples/1_3_functions.js
new file mode 100644
index 0000000..6ab360f
--- /dev/null
+++ b/javascript/examples/1_3_functions.js
@@ -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)");
diff --git a/javascript/examples/1_4_functions-hi.js b/javascript/examples/1_4_functions-hi.js
new file mode 100644
index 0000000..bab5578
--- /dev/null
+++ b/javascript/examples/1_4_functions-hi.js
@@ -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)");
diff --git a/javascript/examples/1_5_vectors.js b/javascript/examples/1_5_vectors.js
new file mode 100644
index 0000000..c1db1d5
--- /dev/null
+++ b/javascript/examples/1_5_vectors.js
@@ -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]");
diff --git a/javascript/functionalExpression.js b/javascript/functionalExpression.js
new file mode 100644
index 0000000..7e6dba6
--- /dev/null
+++ b/javascript/functionalExpression.js
@@ -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];
+};
diff --git a/javascript/graal/collections-25.0.2.jar b/javascript/graal/collections-25.0.2.jar
new file mode 100644
index 0000000..8f90179
Binary files /dev/null and b/javascript/graal/collections-25.0.2.jar differ
diff --git a/javascript/graal/icu4j-25.0.2.jar b/javascript/graal/icu4j-25.0.2.jar
new file mode 100644
index 0000000..6247ca8
Binary files /dev/null and b/javascript/graal/icu4j-25.0.2.jar differ
diff --git a/javascript/graal/jniutils-25.0.2.jar b/javascript/graal/jniutils-25.0.2.jar
new file mode 100644
index 0000000..2cd3187
Binary files /dev/null and b/javascript/graal/jniutils-25.0.2.jar differ
diff --git a/javascript/graal/js-language-25.0.2.jar b/javascript/graal/js-language-25.0.2.jar
new file mode 100644
index 0000000..fce0355
Binary files /dev/null and b/javascript/graal/js-language-25.0.2.jar differ
diff --git a/javascript/graal/js-scriptengine-25.0.2.jar b/javascript/graal/js-scriptengine-25.0.2.jar
new file mode 100644
index 0000000..91b33ab
Binary files /dev/null and b/javascript/graal/js-scriptengine-25.0.2.jar differ
diff --git a/javascript/graal/nativeimage-25.0.2.jar b/javascript/graal/nativeimage-25.0.2.jar
new file mode 100644
index 0000000..3c659e2
Binary files /dev/null and b/javascript/graal/nativeimage-25.0.2.jar differ
diff --git a/javascript/graal/polyglot-25.0.2.jar b/javascript/graal/polyglot-25.0.2.jar
new file mode 100644
index 0000000..064ac60
Binary files /dev/null and b/javascript/graal/polyglot-25.0.2.jar differ
diff --git a/javascript/graal/regex-25.0.2.jar b/javascript/graal/regex-25.0.2.jar
new file mode 100644
index 0000000..845207d
Binary files /dev/null and b/javascript/graal/regex-25.0.2.jar differ
diff --git a/javascript/graal/truffle-api-25.0.2.jar b/javascript/graal/truffle-api-25.0.2.jar
new file mode 100644
index 0000000..f316467
Binary files /dev/null and b/javascript/graal/truffle-api-25.0.2.jar differ
diff --git a/javascript/graal/truffle-compiler-25.0.2.jar b/javascript/graal/truffle-compiler-25.0.2.jar
new file mode 100644
index 0000000..74b0ce3
Binary files /dev/null and b/javascript/graal/truffle-compiler-25.0.2.jar differ
diff --git a/javascript/graal/truffle-runtime-25.0.2.jar b/javascript/graal/truffle-runtime-25.0.2.jar
new file mode 100644
index 0000000..41b2034
Binary files /dev/null and b/javascript/graal/truffle-runtime-25.0.2.jar differ
diff --git a/javascript/graal/word-25.0.2.jar b/javascript/graal/word-25.0.2.jar
new file mode 100644
index 0000000..ac6cf3e
Binary files /dev/null and b/javascript/graal/word-25.0.2.jar differ
diff --git a/javascript/graal/xz-25.0.2.jar b/javascript/graal/xz-25.0.2.jar
new file mode 100644
index 0000000..77498ee
Binary files /dev/null and b/javascript/graal/xz-25.0.2.jar differ
diff --git a/javascript/jstest/JSEngine.java b/javascript/jstest/JSEngine.java
new file mode 100644
index 0000000..8abf14e
--- /dev/null
+++ b/javascript/jstest/JSEngine.java
@@ -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=/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  - 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   Engine.Result eval(final String context, final String code, final Class 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);
+        }
+    }
+}
diff --git a/javascript/jstest/JSExpressionEngine.java b/javascript/jstest/JSExpressionEngine.java
new file mode 100644
index 0000000..e81df8d
--- /dev/null
+++ b/javascript/jstest/JSExpressionEngine.java
@@ -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 {
+    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 prepare(final String expression) {
+        return parse("eval", expression);
+    }
+
+    @Override
+    public Result parse(final String expression) {
+        return parse(parse, expression);
+    }
+
+    private Result parse(final String parse, final String expression) {
+        return engine.eval(expression, "%s(\"%s\")".formatted(parse, expression), Object.class);
+    }
+
+    @Override
+    public Result evaluate(final Result 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 toString(final Result prepared) {
+        return evaluate(prepared, "expr." + toString + "()", String.class);
+    }
+
+    protected  Engine.Result evaluate(
+            final Engine.Result prepared,
+            final String code,
+            final Class result
+    ) {
+        engine.set("expr", prepared);
+        return engine.eval(
+                "%n    in %s%n    where expr = %s%n".formatted(code, prepared.context()),
+                code,
+                result
+        );
+    }
+}
diff --git a/javascript/jstest/example/ExampleTest.java b/javascript/jstest/example/ExampleTest.java
new file mode 100644
index 0000000..5b2970a
--- /dev/null
+++ b/javascript/jstest/example/ExampleTest.java
@@ -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 Programming Paradigms 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  void test(final String code, final Class type, final T expected) {
+            counter.test(() -> Asserts.assertEquals(code, expected, eval(code, type)));
+        }
+
+        public  T eval(final String code, final Class 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);
+    }
+}
diff --git a/javascript/jstest/functional/FunctionalTest.java b/javascript/jstest/functional/FunctionalTest.java
new file mode 100644
index 0000000..50a9202
--- /dev/null
+++ b/javascript/jstest/functional/FunctionalTest.java
@@ -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
+ * JavaScript Functional Expressions
+ * homework of Programming Paradigms 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 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 tester(final TestCounter counter, final Language language) {
+        return tester(counter, language, counter.mode() >= 1, SCRIPT);
+    }
+
+    /* package-private */ static ExprTester 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()
+        );
+    }
+}
diff --git a/javascript/jstest/object/ObjectTest.java b/javascript/jstest/object/ObjectTest.java
new file mode 100644
index 0000000..37f3919
--- /dev/null
+++ b/javascript/jstest/object/ObjectTest.java
@@ -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
+ * JavaScript Object Expressions
+ * homework of Programming Paradigms course.
+ *
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public enum ObjectTest {
+    ;
+
+    /* package-private */
+    static Selector.Composite 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);
+    }
+}
+
diff --git a/javascript/jstest/object/ObjectTester.java b/javascript/jstest/object/ObjectTester.java
new file mode 100644
index 0000000..1c691ab
--- /dev/null
+++ b/javascript/jstest/object/ObjectTester.java
@@ -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
+ * JavaScript Object Expressions
+ * homework of Programming Paradigms 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 tester(
+            final TestCounter counter,
+            final Language language,
+            final String toString,
+            final String parse,
+            final ExprTester.Generator spoiler,
+            final ExprTester.Generator corruptor
+    ) {
+        final ExprTester 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;
+    }
+}
+
diff --git a/javascript/objectExpression.js b/javascript/objectExpression.js
new file mode 100644
index 0000000..9ca2c1d
--- /dev/null
+++ b/javascript/objectExpression.js
@@ -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,
+  };
+}
diff --git a/lectures/README.md b/lectures/README.md
new file mode 100644
index 0000000..e2df236
--- /dev/null
+++ b/lectures/README.md
@@ -0,0 +1,292 @@
+# Лекция 1. Программирование по контракту.
+
+Мы видим следующий код: 
+
+```java
+public class Magic {
+    int magic(int a, int n) {
+        int r = 1;
+        while (n != 0) {
+            if (n % 2 == 1) {
+                r *= a;
+            }
+            n /= 2;
+            a *= a;
+        }
+        return r;
+    }
+}
+```
+
+Что это за код?
+
+Как понять: 
+
+1. Пристально посмотреть.
+2. Написать тесты.
+    * Это не поможет понять, работает ли код. Тесты могут только показать, что код **НЕ работает**.
+3. Написать формальное доказательство.
+
+Для последнего пункта нам понадобится *теорема о структурной декомпозиции*.
+
+**Формулировка**: Любой код, который мы можем написать на каком-то языке, можно представить в виде замыкания следующих операций: 
+
+1. Ничего
+2. Последовательное исполнение операций (последовательность действий)
+3. Присваивание
+4. Ветвление
+5. Цикл `while`
+
+С точки зрения теоремы, этих действий достаточно, чтобы написать любую содержательную программу.
+
+Можно ли ввести такую конструкцию при помощи примитивов выше:
+
+```java
+try {
+    // some code here
+} catch (...) {
+    // some code here
+} finally {
+    // some code here
+}
+```
+
+Давайте введем переменную `exitCode` и для каждого действия, в зависимости от того, успешно оно выполнилось или нет, будем обновлять `exitCode`. 
+
+```java
+if (exitCode == 0) { // без ошибок
+    // делаем дальше
+} else {
+    // ничего не делаем
+}
+```
+
+Попытаемся этим воспользоваться.
+
+Мы хотим наложить какие-то условия на нашу функцию, так что если эти условия на вход выполняются, то мы получим гарантированно правильное значение. Чтобы это доказать, нам помогут *тройки Хоара*. 
+
+Хоар придумал quick sort.
+
+Тройка состоит из `P`, `C` и `Q`, где 
+
+* `P` - пред-условие
+* `C` - код,
+* `Q` - пост-условие
+
+Если у нас есть код `C`, и мы выполняем какое-то пред-условие, то после исполнения кода, у нас будет выполнено какое-то пост-условие.
+
+Для операции *ничего*: 
+
+```java 
+/*
+Pred: Condition
+Code: ;
+Post: Condition
+*/
+```
+
+То условие, которое было до того, как мы сделали *ничего*, останется.
+
+Для *последовательности действий*:
+
+```java 
+/*
+Pred: P1
+Code: ...
+Post: Q1
+Pred: P2
+Code: ...
+Post: Q2
+*/
+```
+
+Таким образом из `Q1` должно следовать `P2`.
+
+```java
+/*
+Pred: P1
+Q1 -> P2
+Post: Q2
+*/
+```
+
+Для *присваивания*: 
+
+```java 
+/*
+Pred: Q[x = expr]
+Code: x = expr
+Post: Q
+*/
+```
+
+Например
+
+```java
+// Pred: (x = a + 2)[x = 6] -> a = 4
+x = a + 2
+// Post: x = 6
+```
+
+То есть только при `a == 4`, выполнится пост-условие.
+
+Для операции `ветвление`: 
+
+```java
+/*
+
+// Pred: cond && P1 || ~cond && P2
+if (cond) {
+    // Pred: P1
+    ...
+    // Post: Q1
+} else {
+    // Pred: P2
+    ...
+    // Post: Q2
+}
+// Q1 -> Q && Q2 -> Q
+// Post: Q
+*/
+```
+
+Для цикла `while`:
+
+```java
+/*
+// Pred: P = I
+while (cond) {
+    // Pred: I
+    // Post: I (инвариант цикла)
+}
+// Post: Q = I
+*/
+```
+
+Посмотрим на примере: 
+
+```java
+// Pred: true
+// Post: r = A' ^ N'
+int magic(int a, int n) {
+    // A' = a, N' = n -- начальные значения
+
+
+    // Pred: A' == a && N' == n
+    int r = 1;
+    // Post: r == 1 && a ** n * r == A' ** N'
+
+
+    // Pred: a ** n * r == A' ** N'
+    // Inv: (a ** n) * r = A' ** N' 
+    while (n != 0) {
+        // Pred: a ** n * r == A' ** N'
+        if (n % 2 == 1) {
+            // Pred: a ** n * r == A' ** N'
+            r = r * a;
+            // Post: a ** (n - 1) * r = A' ** N'
+
+            // Pred: a ** (n - 1) * r = A' ** N'
+            n = n - 1;
+            // Post: a ** n * r = A' ** N'
+        }
+        // Post: a ** n * r = A' ** N'
+
+        // Pred: a ** n * r = A' ** N'
+        n /= 2;
+        // Post: a ** (2 * n) * r = A' ** N'
+
+
+        // Pred: a ** (2 * n) * r = A' ** N'
+        a = a * a;
+        // Post: a ** n * r = A' ** N'
+    }
+    // Post: a ** n * r = A' ** N' && n == 0
+
+
+    // Pred: r = A' ** N'
+    return r;
+    // Post: r = A' ** N' 
+}
+```
+
+Мы формально доказали, что метод `magic()` возводит число `a` в степень `n`. Такая функция называется чистой, так как она не зависит от внешних переменных.
+
+То, что мы написали, называется контракт. Участниками контракта являются *пользователь* и *разработчик*. 
+
+Мы, как разработчик, требуем пред-условие, и тогда можем гарантировать, что пост-условие будет выполняться.
+
+`interface` в java -- частный случай контракта.
+
+Это были случаи определения контракта для *чистых* функций. А как действовать в других случаях. Приведем пример
+
+```java
+int x;
+
+// Pred: 
+// Post: 
+int add(int y) {
+    x = x + y;
+    return x;
+}
+```
+
+Определим *модель* как некоторое состояние нашего класса.
+
+```java
+/*
+Model: x (целое число)
+*/
+```
+
+```java
+int x;
+
+// Pred: true
+// Post: x = x' + y (x' -- старый x)
+int add(int y) {
+    x = x + y;
+    return x;
+}
+```
+
+Здесь контракт соблюдается.
+
+А здесь:
+
+```java
+int x;
+
+// Pred: true
+// Post: x = x' + y (x' -- старый x)
+int add(int y) {
+    x = x + y * 2;
+    return x / 2;
+}
+```
+
+Контракт также соблюдается. То есть нам не важны детали реализации. 
+Можно даже сделать вот так: 
+
+```java
+private int x = 10;
+
+// Post: x = 0
+void init() {
+    x = 1;
+}
+
+// Pred: true
+// Post: x = x' + y
+int add(int y) {
+    x = x + y * 2;
+    return (x - 1) / 2;
+}
+
+// Pred: true
+// Post: R := x
+int get() {
+    return (x - 1) / 2;
+}
+```
+
diff --git a/lectures/lec1/Magic.java b/lectures/lec1/Magic.java
new file mode 100644
index 0000000..7d801d1
--- /dev/null
+++ b/lectures/lec1/Magic.java
@@ -0,0 +1,19 @@
+public class Magic {
+
+  public static void main(String[] args) {
+    System.out.println(magic());
+  }
+
+  int magic(int a, int n) {
+      int r = 1;
+      while (n != 0) {
+          if (n % 2 == 1) {
+              r *= a;
+          }
+          n /= 2;
+          a *= a;
+      }
+      return r;
+  }
+}
+