├── src ├── main │ └── java │ │ ├── module-info.java │ │ └── com.github.forax.exotic │ │ ├── Thrower.java │ │ ├── StringSwitch.java │ │ ├── TypeSwitch.java │ │ ├── VisitorCallSite.java │ │ ├── StringSwitchCallSite.java │ │ ├── ObjectSupport.java │ │ ├── Visitor.java │ │ ├── MostlyConstant.java │ │ ├── StructuralCallImpl.java │ │ ├── TypeSwitchCallSite.java │ │ ├── StructuralCall.java │ │ ├── ConstantMemoizer.java │ │ ├── ObjectSupportLambdas.java │ │ └── StableField.java └── test │ └── java │ ├── com │ └── github │ │ └── forax │ │ └── exotic │ │ └── noaccess │ │ └── NoAccess.java │ └── com.github.forax.exotic │ ├── ConstantMemoizerExampleTests.java │ ├── MostlyConstantExampleTests.java │ ├── TypeSwitchExampleTests.java │ ├── StringSwitchExampleTests.java │ ├── StructuralCallExample2Tests.java │ ├── StructuralCallExampleTests.java │ ├── StableFieldExample2Tests.java │ ├── StableFieldExampleTests.java │ ├── ObjectSupportExampleTests.java │ ├── StringSwitchTests.java │ ├── perf │ ├── ConstantAccessBenchMark.java │ ├── MethodCallBenchMark.java │ ├── FieldAccessBenchMark.java │ ├── ObjectSupportBenchMark.java │ ├── TypeSwitchBenchMark.java │ ├── VisitorBenchMark.java │ └── StringSwitchBenchMark.java │ ├── TypeSwitchTests.java │ ├── VisitorTests.java │ ├── StructuralCallTests.java │ ├── MostlyConstantTests.java │ ├── ConstantMemoizerTests.java │ ├── StableFieldTests.java │ └── ObjectSupportTests.java ├── jitpack.yml ├── .project ├── .gitignore ├── .settings └── org.eclipse.jdt.core.prefs ├── .github └── workflows │ └── main.yml ├── LICENSE ├── .classpath ├── pom.xml └── README.md /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.github.forax.exotic { 2 | requires static jdk.unsupported; // optional if Java 15+ 3 | 4 | exports com.github.forax.exotic; 5 | } 6 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - sdk update 3 | - sdk install java 4 | - sdk default java 5 | - sdk install maven 6 | - export JAVA_HOME=${SDKMAN_DIR}/candidates/java/current 7 | - echo $JAVA_HOME 8 | install: 9 | - mvn --version 10 | - mvn install -DskipTests 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/forax/exotic/noaccess/NoAccess.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic.noaccess; 2 | 3 | import com.github.forax.exotic.StructuralCallTests; 4 | 5 | /** 6 | * A public class with a private method. 7 | * 8 | * @see StructuralCallTests#cannotAccessToAPrivateMethod() 9 | */ 10 | public class NoAccess { 11 | @SuppressWarnings("unused") 12 | private String m(String s) { 13 | return s; 14 | } 15 | } -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | exotic2 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | /target/ 25 | /pro/ 26 | /deps/ 27 | /eclipse-output/ 28 | /eclipse-output-test/ 29 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/Thrower.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | interface Thrower { 4 | RuntimeException magic(Throwable throwable) throws T; 5 | 6 | @SuppressWarnings("unchecked") 7 | static RuntimeException rethrow(Throwable t) { 8 | // works thanks to erasure and checked exception being a compiler thing, not a VM thing 9 | return ((Thrower) 10 | (Thrower) 11 | (Thrower) 12 | e -> { 13 | throw e; 14 | }) 15 | .magic(t); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=9 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=9 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 11 | org.eclipse.jdt.core.compiler.source=9 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | java: [ '11', '15', '17', '20' ] 17 | name: Java ${{ matrix.Java }} sample 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup java 21 | uses: actions/setup-java@v3 22 | with: 23 | distribution: 'zulu' 24 | java-version: ${{ matrix.java }} 25 | - name: build 26 | run: mvn -B package 27 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/ConstantMemoizerExampleTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.function.ToIntFunction; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | @SuppressWarnings("static-method") 10 | public class ConstantMemoizerExampleTests { 11 | private static final ToIntFunction MEMOIZER = 12 | ConstantMemoizer.intMemoizer(Level::ordinal, Level.class); 13 | 14 | enum Level { 15 | LOW, 16 | HIGH 17 | } 18 | 19 | @Test 20 | public void test() { 21 | assertEquals(0, MEMOIZER.applyAsInt(Level.LOW)); 22 | assertEquals(0, MEMOIZER.applyAsInt(Level.LOW)); 23 | assertEquals(1, MEMOIZER.applyAsInt(Level.HIGH)); 24 | assertEquals(1, MEMOIZER.applyAsInt(Level.HIGH)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/MostlyConstantExampleTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.function.IntSupplier; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | @SuppressWarnings("static-method") 10 | public class MostlyConstantExampleTests { 11 | private static final MostlyConstant FOO = new MostlyConstant<>(42, int.class); 12 | private static final IntSupplier FOO_GETTER = FOO.intGetter(); 13 | 14 | public static int getFoo() { 15 | return FOO_GETTER.getAsInt(); 16 | } 17 | 18 | public static void setFoo(int value) { 19 | FOO.setAndDeoptimize(value); 20 | } 21 | 22 | @Test 23 | public void test() { 24 | assertEquals(42, getFoo()); 25 | setFoo(43); 26 | assertEquals(43, getFoo()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/TypeSwitchExampleTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | @SuppressWarnings("static-method") 8 | public class TypeSwitchExampleTests { 9 | private static final TypeSwitch TYPE_SWITCH = TypeSwitch.create(true, Integer.class, String.class); 10 | 11 | public static String asString(Object o) { 12 | switch(TYPE_SWITCH.typeSwitch(o)) { 13 | case TypeSwitch.NULL_MATCH: 14 | return "null"; 15 | case 0: 16 | return "Integer"; 17 | case 1: 18 | return "String"; 19 | default: // TypeSwitch.BAD_MATCH 20 | return "unknown"; 21 | } 22 | } 23 | 24 | @Test 25 | public void example() { 26 | assertEquals("null", asString(null)); 27 | assertEquals("Integer", asString(3)); 28 | assertEquals("String", asString("foo")); 29 | assertEquals("unknown", asString(4.5)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/StringSwitchExampleTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | @SuppressWarnings("static-method") 8 | public class StringSwitchExampleTests { 9 | private static final StringSwitch STRING_SWITCH = StringSwitch.create(true, "bernie the dog", "zara the cat"); 10 | 11 | public static String owner(String s) { 12 | switch(STRING_SWITCH.stringSwitch(s)) { 13 | case StringSwitch.NULL_MATCH: 14 | return "no owner"; 15 | case 0: 16 | return "john"; 17 | case 1: 18 | return "jane"; 19 | default: // TypeSwitch.BAD_MATCH 20 | return "unknown owner"; 21 | } 22 | } 23 | 24 | @Test 25 | public void example() { 26 | assertEquals("no owner", owner(null)); 27 | assertEquals("john", owner("bernie the dog")); 28 | assertEquals("jane", owner("zara the cat")); 29 | assertEquals("unknown owner", owner("foo")); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/StructuralCallExample2Tests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static java.lang.invoke.MethodType.methodType; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | @SuppressWarnings("static-method") 10 | public class StructuralCallExample2Tests { 11 | private static final StructuralCall FOO = 12 | StructuralCall.create(lookup(), "foo", methodType(int.class, String.class)); 13 | 14 | static class A { 15 | int foo(String s) { 16 | return s.length() * 1; 17 | } 18 | } 19 | 20 | static class B { 21 | int foo(String s) { 22 | return s.length() * 2; 23 | } 24 | } 25 | 26 | static int foo(Object o, String s) { 27 | return FOO.invoke(o, s); 28 | } 29 | 30 | @Test 31 | public void test() { 32 | assertEquals(5, foo(new A(), "hello")); 33 | assertEquals(30, foo(new B(), "structural call")); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/StructuralCallExampleTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.lang.invoke.MethodHandles; 7 | import java.lang.invoke.MethodType; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Set; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | @SuppressWarnings("static-method") 15 | public class StructuralCallExampleTests { 16 | private static final StructuralCall IS_EMPTY = 17 | StructuralCall.create( 18 | MethodHandles.lookup(), "isEmpty", MethodType.methodType(boolean.class)); 19 | 20 | static boolean isEmpty(Object o) { 21 | return IS_EMPTY.invoke(o); 22 | } 23 | 24 | @Test 25 | public void test() { 26 | assertTrue(isEmpty(List.of())); 27 | assertFalse(isEmpty(List.of(1))); 28 | assertTrue(isEmpty(Set.of())); 29 | assertTrue(isEmpty(Map.of())); 30 | assertTrue(isEmpty("")); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/StableFieldExample2Tests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import java.util.function.Function; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | @SuppressWarnings("static-method") 11 | public class StableFieldExample2Tests { 12 | enum Option { 13 | a, 14 | b; 15 | 16 | private static final Function UPPERCASE = 17 | StableField.getter(lookup(), Option.class, "uppercase", String.class); 18 | 19 | @SuppressWarnings("unused") 20 | private String uppercase; // stable 21 | 22 | public String upperCase() { 23 | String uppercase = UPPERCASE.apply(this); 24 | if (uppercase != null) { 25 | return uppercase; 26 | } 27 | return this.uppercase = name().toUpperCase(); 28 | } 29 | } 30 | 31 | @Test 32 | public void test() { 33 | assertEquals("A", Option.a.upperCase()); 34 | assertEquals("B", Option.b.upperCase()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/StableFieldExampleTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.util.function.ToIntFunction; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | @SuppressWarnings("static-method") 11 | public class StableFieldExampleTests { 12 | static class SystemInfo { 13 | static final ToIntFunction CPU_COUNT = 14 | StableField.intGetter(lookup(), SystemInfo.class, "cpuCount"); 15 | static final SystemInfo INSTANCE = new SystemInfo(); 16 | 17 | private SystemInfo() { 18 | // enforce singleton 19 | } 20 | 21 | int cpuCount; // stable 22 | 23 | public int getCpuCount() { 24 | int cpuCount = CPU_COUNT.applyAsInt(this); 25 | if (cpuCount == 0) { 26 | return this.cpuCount = Runtime.getRuntime().availableProcessors(); 27 | } 28 | return cpuCount; 29 | } 30 | } 31 | 32 | @Test 33 | public void test() { 34 | assertTrue(SystemInfo.INSTANCE.getCpuCount() > 0); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Rémi Forax 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/ObjectSupportExampleTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | @SuppressWarnings("static-method") 10 | public class ObjectSupportExampleTests { 11 | static final class Person { 12 | private static final ObjectSupport SUPPORT = ObjectSupport.of(lookup(), Person.class, p -> p.name, p -> p.age); 13 | 14 | private final String name; 15 | private final int age; 16 | 17 | public Person(String name, int age) { 18 | this.name = name; 19 | this.age = age; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object other) { 24 | return SUPPORT.equals(this, other); 25 | } 26 | 27 | @Override 28 | public int hashCode() { 29 | return SUPPORT.hashCode(this); 30 | } 31 | } 32 | 33 | @Test 34 | public void testEqualsPerson() { 35 | Person person1 = new Person("bob", 34); 36 | Person person2 = new Person("cathy", 27); 37 | Person person3 = new Person("bob", 34); 38 | assertNotEquals(person1, person2); 39 | assertEquals(person1, person3); 40 | } 41 | 42 | @Test 43 | public void testHashCodePerson() { 44 | Person person1 = new Person("cathy", 27); 45 | Person person2 = new Person("cathy", 27); 46 | assertEquals(person1.hashCode(), person2.hashCode()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/StringSwitchTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | @SuppressWarnings("static-method") 10 | public class StringSwitchTests { 11 | @Test 12 | public void simple() { 13 | StringSwitch stringSwitch = StringSwitch.create(false, "foo", "bar"); 14 | assertAll( 15 | () -> assertEquals(0, stringSwitch.stringSwitch("foo")), 16 | () -> assertEquals(0, stringSwitch.stringSwitch(new String("foo"))), 17 | () -> assertEquals(1, stringSwitch.stringSwitch("bar")), 18 | () -> assertEquals(1, stringSwitch.stringSwitch(new String("bar"))), 19 | () -> assertEquals(StringSwitch.NO_MATCH, stringSwitch.stringSwitch("baz")) 20 | ); 21 | } 22 | 23 | @Test 24 | public void nonNullSwitchCalledWithANull() { 25 | StringSwitch stringSwitch = StringSwitch.create(false); 26 | assertThrows(NullPointerException.class, () -> stringSwitch.stringSwitch(null)); 27 | } 28 | 29 | @Test 30 | public void nullCase() { 31 | StringSwitch stringSwitch = StringSwitch.create(true, "foo"); 32 | assertAll( 33 | () -> assertEquals(0, stringSwitch.stringSwitch("foo")), 34 | () -> assertEquals(StringSwitch.NULL_MATCH, stringSwitch.stringSwitch(null)), 35 | () -> assertEquals(StringSwitch.NO_MATCH, stringSwitch.stringSwitch("")) 36 | ); 37 | } 38 | 39 | @Test 40 | public void aCaseCanNotBeNull() { 41 | assertAll( 42 | () -> assertThrows(NullPointerException.class, () -> StringSwitch.create(false, (String)null)), 43 | () -> assertThrows(NullPointerException.class, () -> StringSwitch.create(true, (String)null)) 44 | ); 45 | } 46 | 47 | @Test 48 | public void casesArrayCanNotBeNull() { 49 | assertThrows(NullPointerException.class, () -> StringSwitch.create(false, (String[])null)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/perf/ConstantAccessBenchMark.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic.perf; 2 | 3 | import com.github.forax.exotic.MostlyConstant; 4 | import java.util.concurrent.TimeUnit; 5 | import java.util.function.IntSupplier; 6 | import java.util.function.Supplier; 7 | import org.openjdk.jmh.annotations.Benchmark; 8 | import org.openjdk.jmh.annotations.BenchmarkMode; 9 | import org.openjdk.jmh.annotations.Fork; 10 | import org.openjdk.jmh.annotations.Measurement; 11 | import org.openjdk.jmh.annotations.Mode; 12 | import org.openjdk.jmh.annotations.OutputTimeUnit; 13 | import org.openjdk.jmh.annotations.Scope; 14 | import org.openjdk.jmh.annotations.State; 15 | import org.openjdk.jmh.annotations.Warmup; 16 | import org.openjdk.jmh.runner.Runner; 17 | import org.openjdk.jmh.runner.RunnerException; 18 | import org.openjdk.jmh.runner.options.Options; 19 | import org.openjdk.jmh.runner.options.OptionsBuilder; 20 | 21 | @SuppressWarnings("static-method") 22 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 23 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 24 | @Fork(3) 25 | @BenchmarkMode(Mode.AverageTime) 26 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 27 | @State(Scope.Benchmark) 28 | public class ConstantAccessBenchMark { 29 | static final int static_final_int = 1_000; 30 | static final Integer static_final_Integer = 1_000; 31 | 32 | private static final MostlyConstant MOSTLY_CONSTANT_INT = 33 | new MostlyConstant<>(1_000, int.class); 34 | private static final IntSupplier MOSTLY_CONSTANT_INT_GETTER = MOSTLY_CONSTANT_INT.intGetter(); 35 | 36 | private static final MostlyConstant MOSTLY_CONSTANT_INTEGER = 37 | new MostlyConstant<>(1_000, Integer.class); 38 | private static final Supplier MOSTLY_CONSTANT_INTEGER_GETTER = 39 | MOSTLY_CONSTANT_INTEGER.getter(); 40 | 41 | @Benchmark 42 | public int static_final_int() { 43 | return 1_000 / static_final_int; 44 | } 45 | 46 | @Benchmark 47 | public int static_final_Integer() { 48 | return 1_000 / static_final_Integer; 49 | } 50 | 51 | @Benchmark 52 | public int mostly_constant_int() { 53 | return 1_000 / MOSTLY_CONSTANT_INT_GETTER.getAsInt(); 54 | } 55 | 56 | @Benchmark 57 | public int mostly_constant_Integer() { 58 | return 1_000 / MOSTLY_CONSTANT_INTEGER_GETTER.get(); 59 | } 60 | 61 | public static void main(String[] args) throws RunnerException { 62 | Options opt = new OptionsBuilder().include(ConstantAccessBenchMark.class.getName()).build(); 63 | new Runner(opt).run(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/perf/MethodCallBenchMark.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic.perf; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static java.lang.invoke.MethodType.methodType; 5 | 6 | import com.github.forax.exotic.ConstantMemoizer; 7 | import com.github.forax.exotic.StructuralCall; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.function.ToIntFunction; 10 | import org.openjdk.jmh.annotations.Benchmark; 11 | import org.openjdk.jmh.annotations.BenchmarkMode; 12 | import org.openjdk.jmh.annotations.Fork; 13 | import org.openjdk.jmh.annotations.Measurement; 14 | import org.openjdk.jmh.annotations.Mode; 15 | import org.openjdk.jmh.annotations.OutputTimeUnit; 16 | import org.openjdk.jmh.annotations.Scope; 17 | import org.openjdk.jmh.annotations.State; 18 | import org.openjdk.jmh.annotations.Warmup; 19 | import org.openjdk.jmh.runner.Runner; 20 | import org.openjdk.jmh.runner.RunnerException; 21 | import org.openjdk.jmh.runner.options.Options; 22 | import org.openjdk.jmh.runner.options.OptionsBuilder; 23 | 24 | @SuppressWarnings("static-method") 25 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 26 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 27 | @Fork(3) 28 | @BenchmarkMode(Mode.AverageTime) 29 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 30 | @State(Scope.Benchmark) 31 | public class MethodCallBenchMark { 32 | interface I { 33 | int f(); 34 | } 35 | 36 | static class A implements I { 37 | @Override 38 | public int f() { 39 | return 1; 40 | } 41 | } 42 | 43 | static class B implements I { 44 | @Override 45 | public int f() { 46 | return 1; 47 | } 48 | } 49 | 50 | static class C implements I { 51 | @Override 52 | public int f() { 53 | return 1; 54 | } 55 | } 56 | 57 | private static final I[] ARRAY = new I[] {new A(), new B(), new C()}; 58 | 59 | private static final ToIntFunction MEMOIZER = ConstantMemoizer.intMemoizer(I::f, I.class); 60 | 61 | private static final StructuralCall STRUCTURAL_CALL = 62 | StructuralCall.create(lookup(), "f", methodType(int.class)); 63 | 64 | @Benchmark 65 | public int virtual_call() { 66 | int sum = 0; 67 | for (I i : ARRAY) { 68 | sum += i.f(); 69 | } 70 | return sum; 71 | } 72 | 73 | @Benchmark 74 | public int memoizer() { 75 | int sum = 0; 76 | for (I i : ARRAY) { 77 | sum += MEMOIZER.applyAsInt(i); 78 | } 79 | return sum; 80 | } 81 | 82 | @Benchmark 83 | public int structural_call() { 84 | int sum = 0; 85 | for (I i : ARRAY) { 86 | sum += (int) STRUCTURAL_CALL.invoke(i); 87 | } 88 | return sum; 89 | } 90 | 91 | public static void main(String[] args) throws RunnerException { 92 | Options opt = new OptionsBuilder().include(MethodCallBenchMark.class.getName()).build(); 93 | new Runner(opt).run(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/perf/FieldAccessBenchMark.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic.perf; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static java.lang.invoke.MethodType.methodType; 5 | 6 | import com.github.forax.exotic.ConstantMemoizer; 7 | import com.github.forax.exotic.MostlyConstant; 8 | import com.github.forax.exotic.StableField; 9 | import com.github.forax.exotic.StructuralCall; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.function.IntSupplier; 12 | import java.util.function.ToIntFunction; 13 | import org.openjdk.jmh.annotations.Benchmark; 14 | import org.openjdk.jmh.annotations.BenchmarkMode; 15 | import org.openjdk.jmh.annotations.Fork; 16 | import org.openjdk.jmh.annotations.Measurement; 17 | import org.openjdk.jmh.annotations.Mode; 18 | import org.openjdk.jmh.annotations.OutputTimeUnit; 19 | import org.openjdk.jmh.annotations.Scope; 20 | import org.openjdk.jmh.annotations.State; 21 | import org.openjdk.jmh.annotations.Warmup; 22 | import org.openjdk.jmh.runner.Runner; 23 | import org.openjdk.jmh.runner.RunnerException; 24 | import org.openjdk.jmh.runner.options.Options; 25 | import org.openjdk.jmh.runner.options.OptionsBuilder; 26 | 27 | @SuppressWarnings("static-method") 28 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 29 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 30 | @Fork(3) 31 | @BenchmarkMode(Mode.AverageTime) 32 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 33 | @State(Scope.Benchmark) 34 | public class FieldAccessBenchMark { 35 | static final A static_final = new A(1_000); 36 | 37 | static class A { 38 | final int x; 39 | 40 | public A(int x) { 41 | this.x = x; 42 | } 43 | 44 | public int x() { 45 | return x; 46 | } 47 | } 48 | 49 | private static final MostlyConstant MOSTLY_CONSTANT = 50 | new MostlyConstant<>(1_000, int.class); 51 | private static final IntSupplier MOSTLY_CONSTANT_GETTER = MOSTLY_CONSTANT.intGetter(); 52 | 53 | private static final ToIntFunction STABLE_X = StableField.intGetter(lookup(), A.class, "x"); 54 | 55 | private static final ToIntFunction MEMOIZER = ConstantMemoizer.intMemoizer(A::x, A.class); 56 | 57 | private static final StructuralCall STRUCTURAL_CALL = 58 | StructuralCall.create(lookup(), "x", methodType(int.class)); 59 | 60 | @Benchmark 61 | public int field_access() { 62 | return 1_000 / static_final.x; 63 | } 64 | 65 | @Benchmark 66 | public int mostly_constant() { 67 | return 1_000 / MOSTLY_CONSTANT_GETTER.getAsInt(); 68 | } 69 | 70 | @Benchmark 71 | public int stable_field() { 72 | return 1_000 / STABLE_X.applyAsInt(static_final); 73 | } 74 | 75 | @Benchmark 76 | public int memoizer() { 77 | return 1_000 / MEMOIZER.applyAsInt(static_final); 78 | } 79 | 80 | @Benchmark 81 | public int structural_call() { 82 | return 1_000 / (int) STRUCTURAL_CALL.invoke(static_final); 83 | } 84 | 85 | public static void main(String[] args) throws RunnerException { 86 | Options opt = new OptionsBuilder().include(FieldAccessBenchMark.class.getName()).build(); 87 | new Runner(opt).run(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.github.forax 6 | exotic 7 | 1.4.0 8 | 9 | 10 | UTF-8 11 | 12 | 13 | 14 | 15 | org.junit.jupiter 16 | junit-jupiter-api 17 | 5.9.2 18 | test 19 | 20 | 21 | 22 | org.openjdk.jmh 23 | jmh-core 24 | 1.36 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-compiler-plugin 34 | 3.11.0 35 | 36 | 37 | default-compile 38 | 39 | 11 40 | 41 | 42 | 52 | 53 | base-compile 54 | 55 | compile 56 | 57 | 58 | 59 | module-info.java 60 | 61 | 62 | 63 | 64 | 65 | 8 66 | 11 67 | 68 | 11 69 | 70 | 71 | 72 | 73 | 74 | maven-surefire-plugin 75 | 3.0.0 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-javadoc-plugin 81 | 3.5.0 82 | 83 | src/main/java/ 84 | 8 85 | 86 | 87 | 88 | attach-javadocs 89 | 90 | jar 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/StringSwitch.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | 5 | /** 6 | * A StringSwitch allows to encode a switch on strings as a plain old switch on integers. 7 | * For that, a StringSwitch is {@link #create(boolean, String...) created} with an array of strings 8 | * and will try to find for a string the index of the same string in the array. 9 | * 10 | * An example of usage, instead of using a cascade of 'if equals' 11 | *
12 |  * public static String owner(String s) {
13 |  *   if (o == null) {
14 |  *     return "not owner";
15 |  *   }
16 |  *   if (s.equals("bernie the dog") {
17 |  *     return "john";
18 |  *   }
19 |  *   if (s.equals("zara the cat") {
20 |  *     return "jane";
21 |  *   }
22 |  *   // default
23 |  *   return "unknown owner";
24 |  * }
25 |  * 
26 | * 27 | * a StringSwitch allows to use a plain old switch to switch on string 28 | *
29 |  * private static final StringSwitch STRING_SWITCH = StringSwitch.create(true, "bernie the dog", "zara the cat");
30 |  *
31 |  * public static String owner(String s) {
32 |  *   switch(STRING_SWITCH.stringSwitch(s)) {
33 |  *   case StringSwitch.NULL_MATCH:
34 |  *     return "no owner";
35 |  *   case 0:
36 |  *     return "john";
37 |  *   case 1:
38 |  *     return "jane";
39 |  *   default: // TypeSwitch.BAD_MATCH
40 |  *     return "unknown owner";
41 |  *   }
42 |  * }
43 |  * 
44 | * 45 | */ 46 | @FunctionalInterface 47 | public interface StringSwitch { 48 | /** 49 | * Returns the index of the first class in {@code stringcases} that match with the class of {@code value} taken as parameter. 50 | * @param value the value 51 | * @return the index of the first class that match the class of the {@code value}, 52 | * {@value #NULL_MATCH} if {@code value} is null or {@link #NO_MATCH} if no class in the array match the class. 53 | * 54 | * @see #create(boolean, String...) 55 | */ 56 | int stringSwitch(String value); 57 | 58 | /** 59 | * Return value of {@link #stringSwitch(String)} that indicates that no match is found. 60 | */ 61 | int NO_MATCH = -2; 62 | 63 | /** 64 | * Return value of {@link #stringSwitch(String)} that indicates that null is found. 65 | */ 66 | int NULL_MATCH = -1; 67 | 68 | /** 69 | * Creates a StringSwitch that returns for a string the index in the {@code stringcases} array 70 | * or {@link #NO_MATCH} if no string match. 71 | * 72 | * @param nullMatch true is the StringSwitch should allow null. 73 | * @param stringcases an array of string. 74 | * @return a StringSwitch configured with the array of stringcases. 75 | * @throws NullPointerException is {@code stringcases is null} or one string of the array is null. 76 | * @throws IllegalStateException if the same string appears several times in the array. 77 | * 78 | * @see StringSwitch#stringSwitch(String) 79 | */ 80 | static StringSwitch create(boolean nullMatch, String... stringcases) { 81 | MethodHandle mh = StringSwitchCallSite.wrapNullIfNecessary(nullMatch, StringSwitchCallSite.create(stringcases).dynamicInvoker()); 82 | return value -> { 83 | try { 84 | return (int)mh.invokeExact(value); 85 | } catch(Throwable t) { 86 | throw Thrower.rethrow(t); 87 | } 88 | }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/TypeSwitch.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | 5 | /** 6 | * A TypeSwitch allows to encode a switch on types as a plain old switch on integers. 7 | * For that, a TypeSwitch is {@link #create(boolean, Class...) created} with an array of classes 8 | * and will try to find for an object the first class of this array which is a super-type of the object class. 9 | * 10 | * The class inside the array must follow a partial order with the classes more specific (the subtypes) first 11 | * and classes less specific (the supertypes) after them. 12 | * 13 | * An example of usage, instead of using a cascade of 'if instanceof' 14 | *
15 |  * public static String asString(Object o) {
16 |  *   if (o == null) {
17 |  *     return "null";
18 |  *   }
19 |  *   if (o instanceof Integer) {
20 |  *     return "Integer";
21 |  *   }
22 |  *   if (o instanceof String) {
23 |  *     return "String";
24 |  *   }
25 |  *   // default
26 |  *   return "unknown";
27 |  * }
28 |  * 
29 | * 30 | * a TypeSwitch allows to use a plain old switch to switch on type 31 | *
32 |  * private static final TypeSwitch TYPE_SWITCH = TypeSwitch.create(true, Integer.class, String.class);
33 |  * 
34 |  * public static String asString(Object o) {
35 |  *   switch(TYPE_SWITCH.typeSwitch(o)) {
36 |  *   case TypeSwitch.NULL_MATCH:
37 |  *     return "null";
38 |  *   case 0:
39 |  *     return "Integer";
40 |  *   case 1:
41 |  *     return "String";
42 |  *   default: // TypeSwitch.BAD_MATCH
43 |  *     return "unknown";
44 |  *   }
45 |  * }
46 |  * 
47 | * 48 | */ 49 | @FunctionalInterface 50 | public interface TypeSwitch { 51 | /** 52 | * Returns the index of the first class in {@code typecases} that match with the class of {@code value} taken as parameter. 53 | * @param value the value 54 | * @return the index of the first class that match the class of the {@code value}, 55 | * {@value #NULL_MATCH} if {@code value} is null or {@link #NO_MATCH} if no class in the array match the class. 56 | * 57 | * @see #create(boolean, Class...) 58 | */ 59 | int typeSwitch(Object value); 60 | 61 | /** 62 | * Return value of {@link #typeSwitch(Object)} that indicates that no match is found. 63 | */ 64 | int NO_MATCH = -2; 65 | 66 | /** 67 | * Return value of {@link #typeSwitch(Object)} that indicates that null is found. 68 | */ 69 | int NULL_MATCH = -1; 70 | 71 | /** 72 | * Creates a TypeSwitch that returns for an object the index of its class/superclasses in the {@code typecases} array 73 | * or {@link #NO_MATCH} if no class match. 74 | * If several classes in {@code typecases} can match the class of the object, the first class in the array will be chosen. 75 | * 76 | * @param nullMatch true is the TypeSwitch should allow null. 77 | * @param typecases an array 78 | * @return a TypeSwitch configured with the array of typecases. 79 | * @throws NullPointerException is {@code typecases is null} or one element of the array is null. 80 | * 81 | * @see TypeSwitch#typeSwitch(Object) 82 | */ 83 | static TypeSwitch create(boolean nullMatch, Class... typecases) { 84 | TypeSwitchCallSite.validatePartialOrder(typecases); 85 | MethodHandle mh = TypeSwitchCallSite.wrapNullIfNecessary(nullMatch, TypeSwitchCallSite.create(typecases).dynamicInvoker()); 86 | return value -> { 87 | try { 88 | return (int)mh.invokeExact(value); 89 | } catch(Throwable t) { 90 | throw Thrower.rethrow(t); 91 | } 92 | }; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/perf/ObjectSupportBenchMark.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic.perf; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | 5 | import java.util.Objects; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import org.openjdk.jmh.annotations.Benchmark; 9 | import org.openjdk.jmh.annotations.BenchmarkMode; 10 | import org.openjdk.jmh.annotations.Fork; 11 | import org.openjdk.jmh.annotations.Measurement; 12 | import org.openjdk.jmh.annotations.Mode; 13 | import org.openjdk.jmh.annotations.OutputTimeUnit; 14 | import org.openjdk.jmh.annotations.Scope; 15 | import org.openjdk.jmh.annotations.State; 16 | import org.openjdk.jmh.annotations.Warmup; 17 | import org.openjdk.jmh.runner.Runner; 18 | import org.openjdk.jmh.runner.RunnerException; 19 | import org.openjdk.jmh.runner.options.Options; 20 | import org.openjdk.jmh.runner.options.OptionsBuilder; 21 | 22 | import com.github.forax.exotic.ObjectSupport; 23 | 24 | @SuppressWarnings("static-method") 25 | @Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) 26 | @Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) 27 | @Fork(3) 28 | @BenchmarkMode(Mode.AverageTime) 29 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 30 | @State(Scope.Benchmark) 31 | public class ObjectSupportBenchMark { 32 | static final class AutoPerson { 33 | private static final ObjectSupport SUPPORT = ObjectSupport.of(lookup(), AutoPerson.class, p -> p.name, p -> p.age); 34 | 35 | private final String name; 36 | private final int age; 37 | 38 | public AutoPerson(String name, int age) { 39 | this.name = name; 40 | this.age = age; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object other) { 45 | return SUPPORT.equals(this, other); 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | return SUPPORT.hashCode(this); 51 | } 52 | } 53 | 54 | static final class HandWrittenPerson { 55 | private final String name; 56 | private final int age; 57 | 58 | public HandWrittenPerson(String name, int age) { 59 | this.name = name; 60 | this.age = age; 61 | } 62 | 63 | @Override 64 | public boolean equals(Object other) { 65 | if (!(other instanceof HandWrittenPerson)) { 66 | return false; 67 | } 68 | HandWrittenPerson person = (HandWrittenPerson) other; 69 | return Objects.equals(name, person.name) && age == person.age; 70 | } 71 | 72 | @Override 73 | public int hashCode() { 74 | return (1 * 63 + Objects.hashCode(name)) * 63 + age; 75 | } 76 | } 77 | 78 | private static final AutoPerson AUTO_PERSON1 = new AutoPerson("martin", 68); 79 | private static final AutoPerson AUTO_PERSON2 = new AutoPerson("martin", 68); 80 | private static final HandWrittenPerson HAND_WRITTEN_PERSON1 = new HandWrittenPerson("martin", 68); 81 | private static final HandWrittenPerson HAND_WRITTEN_PERSON2 = new HandWrittenPerson("martin", 68); 82 | 83 | @Benchmark 84 | public boolean auto_equals() { 85 | return AUTO_PERSON1.equals(AUTO_PERSON2); 86 | } 87 | 88 | @Benchmark 89 | public boolean hand_written_equals() { 90 | return HAND_WRITTEN_PERSON1.equals(HAND_WRITTEN_PERSON2); 91 | } 92 | 93 | @Benchmark 94 | public int auto_hashCode() { 95 | return AUTO_PERSON1.hashCode(); 96 | } 97 | 98 | @Benchmark 99 | public int hand_written_hashCode() { 100 | return HAND_WRITTEN_PERSON1.hashCode(); 101 | } 102 | 103 | 104 | public static void main(String[] args) throws RunnerException { 105 | Options opt = new OptionsBuilder().include(ObjectSupportBenchMark.class.getName()).build(); 106 | new Runner(opt).run(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/TypeSwitchTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | import java.io.Serializable; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | @SuppressWarnings("static-method") 12 | public class TypeSwitchTests { 13 | @Test 14 | public void simple() { 15 | TypeSwitch typeSwitch = TypeSwitch.create(false, Integer.class, String.class); 16 | assertAll( 17 | () -> assertEquals(0, typeSwitch.typeSwitch(3)), 18 | () -> assertEquals(0, typeSwitch.typeSwitch(42)), 19 | () -> assertEquals(1, typeSwitch.typeSwitch("foo")), 20 | () -> assertEquals(1, typeSwitch.typeSwitch("bar")), 21 | () -> assertEquals(TypeSwitch.NO_MATCH, typeSwitch.typeSwitch(4.5)) 22 | ); 23 | } 24 | 25 | @Test 26 | public void inheritance() { 27 | TypeSwitch typeSwitch = TypeSwitch.create(false, CharSequence.class, Object.class); 28 | assertAll( 29 | () -> assertEquals(1, typeSwitch.typeSwitch(3)), 30 | () -> assertEquals(1, typeSwitch.typeSwitch(42)), 31 | () -> assertEquals(0, typeSwitch.typeSwitch("foo")), 32 | () -> assertEquals(0, typeSwitch.typeSwitch("bar")), 33 | () -> assertEquals(1, typeSwitch.typeSwitch(4.5)) 34 | ); 35 | } 36 | 37 | interface I { /*empty*/ } 38 | interface J { /*empty*/ } 39 | class A implements I, J { /*empty*/ } 40 | 41 | @Test 42 | public void interfaces() { 43 | TypeSwitch typeSwitch = TypeSwitch.create(false, I.class, J.class); 44 | assertAll( 45 | () -> assertEquals(0, typeSwitch.typeSwitch(new A())), 46 | () -> assertEquals(0, typeSwitch.typeSwitch(new A())), 47 | () -> assertEquals(TypeSwitch.NO_MATCH, typeSwitch.typeSwitch("bar")) 48 | ); 49 | } 50 | 51 | @Test 52 | public void interfaces2() { 53 | TypeSwitch typeSwitch = TypeSwitch.create(false, J.class, I.class); 54 | assertAll( 55 | () -> assertEquals(0, typeSwitch.typeSwitch(new A())), 56 | () -> assertEquals(0, typeSwitch.typeSwitch(new A())), 57 | () -> assertEquals(TypeSwitch.NO_MATCH, typeSwitch.typeSwitch("bar")) 58 | ); 59 | } 60 | 61 | @Test 62 | public void nonNullSwitchCalledWithANull() { 63 | TypeSwitch typeSwitch = TypeSwitch.create(false); 64 | assertThrows(NullPointerException.class, () -> typeSwitch.typeSwitch(null)); 65 | } 66 | 67 | @Test 68 | public void nullCase() { 69 | TypeSwitch typeSwitch = TypeSwitch.create(true, String.class); 70 | assertAll( 71 | () -> assertEquals(0, typeSwitch.typeSwitch("foo")), 72 | () -> assertEquals(TypeSwitch.NULL_MATCH, typeSwitch.typeSwitch(null)), 73 | () -> assertEquals(TypeSwitch.NO_MATCH, typeSwitch.typeSwitch(3)) 74 | ); 75 | } 76 | 77 | @Test 78 | public void aCaseCanNotBeNull() { 79 | assertAll( 80 | () -> assertThrows(NullPointerException.class, () -> TypeSwitch.create(false, (Class)null)), 81 | () -> assertThrows(NullPointerException.class, () -> TypeSwitch.create(true, (Class)null)) 82 | ); 83 | } 84 | 85 | @Test 86 | public void invalidPartialOrder() { 87 | assertAll( 88 | () -> assertThrows(IllegalStateException.class, () -> TypeSwitch.create(false, Object.class, String.class)), 89 | () -> assertThrows(IllegalStateException.class, () -> TypeSwitch.create(false, Comparable.class, String.class)), 90 | () -> assertThrows(IllegalStateException.class, () -> TypeSwitch.create(false, Object.class, Comparable.class)), 91 | () -> assertThrows(IllegalStateException.class, () -> TypeSwitch.create(false, Serializable.class, Comparable.class, String.class)) 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/VisitorTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertAll; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | import java.util.HashMap; 8 | import java.util.List; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | @SuppressWarnings("static-method") 13 | public class VisitorTests { 14 | 15 | interface Expr { /**/ } 16 | static class Value implements Expr { final int value; Value(int value) { this.value = value; }} 17 | static class Add implements Expr { final Expr left, right; Add(Expr left, Expr right) { this.left = left; this.right = right; }} 18 | static class Var implements Expr { final String name; Var(String name) { this.name = name; }} 19 | static class Assign implements Expr { final String name; final Expr expr; Assign(String name, Expr expr) { this.name = name; this.expr = expr; }} 20 | static class Block implements Expr { final List exprs; Block(List exprs) { this.exprs = exprs; }} 21 | 22 | @Test 23 | public void simple() { 24 | Visitor visitor = Visitor.create(Void.class, int.class, opt -> opt 25 | .register(Value.class, (v, value, __) -> value.value) 26 | .register(Add.class, (v, add, __) -> v.visit(add.left, null) + v.visit(add.right, null)) 27 | ); 28 | Expr expr = new Add(new Add(new Value(7), new Value(10)), new Value(4)); 29 | assertEquals(21, (int)visitor.visit(expr, null)); 30 | } 31 | 32 | @Test 33 | public void interpret() { 34 | class Env { 35 | final HashMap vars = new HashMap<>(); 36 | } 37 | 38 | Visitor visitor = Visitor.create(Env.class, int.class, opt -> opt 39 | .register(Value.class, (v, value, env) -> value.value) 40 | .register(Add.class, (v, add, env) -> v.visit(add.left, env) + v.visit(add.right, env)) 41 | .register(Var.class, (v, var, env) -> env.vars.getOrDefault(var.name, 0)) 42 | .register(Assign.class, (v, assign, env) -> { int let = v.visit(assign.expr, env); env.vars.put(assign.name, let); return let; }) 43 | .register(Block.class, (v, block, env) -> block.exprs.stream().mapToInt(e -> v.visit(e, env)).reduce(0, (v1, v2) -> v2)) 44 | ); 45 | Expr code = new Block(List.of( 46 | new Assign("a", new Value(3)), 47 | new Add(new Add(new Var("a"), new Value(10)), new Value(4)) 48 | )); 49 | assertEquals(17, (int)visitor.visit(code, new Env())); 50 | } 51 | 52 | @Test 53 | public void registerSameVisitletTwice() { 54 | assertThrows(IllegalStateException.class, () -> 55 | Visitor.create(Void.class, Void.class, opt -> opt 56 | .register(String.class, (_1, _2, _3) -> null) 57 | .register(String.class, (_1, _2, _3) -> null) 58 | )); 59 | } 60 | 61 | @Test 62 | public void visitCanNotFindAVisitlet() { 63 | Visitor visitor = Visitor.create(Void.class, Void.class, opt -> { /*empty*/ }); 64 | assertThrows(IllegalStateException.class, () -> 65 | visitor.visit("oops", null)); 66 | } 67 | 68 | @Test 69 | public void nullWhenCreatingAVisitor() { 70 | assertAll( 71 | () -> assertThrows(NullPointerException.class, () -> Visitor.create(null, Void.class, opt -> { /*empty*/ })), 72 | () -> assertThrows(NullPointerException.class, () -> Visitor.create(Void.class, null, opt -> { /*empty*/ })), 73 | () -> assertThrows(NullPointerException.class, () -> Visitor.create(Void.class, Void.class, null)) 74 | ); 75 | } 76 | 77 | @Test 78 | public void nullWhenCallingVisit() { 79 | Visitor visitor = Visitor.create(String.class, Void.class, opt -> { /*empty*/ }); 80 | assertThrows(NullPointerException.class, () -> visitor.visit(null, "hello")); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/VisitorCallSite.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.exactInvoker; 4 | import static java.lang.invoke.MethodHandles.foldArguments; 5 | import static java.lang.invoke.MethodHandles.guardWithTest; 6 | import static java.lang.invoke.MethodType.methodType; 7 | 8 | import java.lang.invoke.MethodHandle; 9 | import java.lang.invoke.MethodHandles; 10 | import java.lang.invoke.MethodHandles.Lookup; 11 | import java.lang.invoke.MethodType; 12 | import java.lang.invoke.MutableCallSite; 13 | import java.util.HashMap; 14 | import java.util.Objects; 15 | 16 | import com.github.forax.exotic.Visitor.Visitlet; 17 | 18 | class VisitorCallSite extends MutableCallSite { 19 | private static final MethodHandle FALLBACK, TYPECHECK, FIND; 20 | static final MethodHandle VISIT; 21 | static { 22 | Lookup lookup = MethodHandles.lookup(); 23 | try { 24 | FALLBACK = lookup.findVirtual(VisitorCallSite.class, "fallback", methodType(MethodHandle.class, Object.class)); 25 | VISIT = lookup.findVirtual(Visitlet.class, "visit", methodType(Object.class, Visitor.class, Object.class, Object.class)); 26 | TYPECHECK = lookup.findStatic(VisitorCallSite.class, "typecheck", methodType(boolean.class, Class.class, Object.class)); 27 | FIND = lookup.findStatic(VisitorCallSite.class, "find", methodType(MethodHandle.class, HashMap.class, Object.class)); 28 | } catch (NoSuchMethodException | IllegalAccessException e) { 29 | throw new AssertionError(e); 30 | } 31 | } 32 | 33 | private static final int MAX_DEPTH = 8; 34 | 35 | static Visitor visitor(MethodType methodType, HashMap, MethodHandle> map) { 36 | MethodHandle mh = new VisitorCallSite(methodType, map) 37 | .dynamicInvoker() 38 | .asType(methodType(Object.class, Object.class, Object.class)); 39 | return (expr, parameter) -> { 40 | Objects.requireNonNull(expr); 41 | try { 42 | return (R)mh.invokeExact(expr, parameter); 43 | } catch(Throwable t) { 44 | throw Thrower.rethrow(t); 45 | } 46 | }; 47 | } 48 | 49 | private final int depth; 50 | private final VisitorCallSite callsite; 51 | private final HashMap, MethodHandle> map; 52 | 53 | private VisitorCallSite(MethodType methodType, HashMap,MethodHandle> map) { 54 | super(methodType); 55 | this.depth = 0; 56 | this.callsite = this; 57 | this.map = map; 58 | setTarget(foldArguments(exactInvoker(methodType), FALLBACK.bindTo(this))); 59 | } 60 | 61 | private VisitorCallSite(MethodType methodType, VisitorCallSite callsite, int depth, HashMap,MethodHandle> map) { 62 | super(methodType); 63 | this.depth = depth; 64 | this.callsite = callsite; 65 | this.map = map; 66 | setTarget(foldArguments(exactInvoker(methodType), FALLBACK.bindTo(this))); 67 | } 68 | 69 | @SuppressWarnings("unused") 70 | private MethodHandle fallback(Object o) { 71 | Class receiverClass = o.getClass(); 72 | MethodHandle target = map.get(receiverClass); 73 | if (target == null) { 74 | throw new IllegalStateException("no visitlet register for type " + receiverClass.getName()); 75 | } 76 | 77 | if (depth == MAX_DEPTH) { 78 | callsite.setTarget(foldArguments(exactInvoker(type()), FIND.bindTo(map))); 79 | } else { 80 | MethodHandle guard = guardWithTest(TYPECHECK.bindTo(receiverClass), 81 | target, 82 | new VisitorCallSite(type(), callsite, depth + 1, map).dynamicInvoker()); 83 | setTarget(guard); 84 | } 85 | 86 | return target; 87 | } 88 | 89 | @SuppressWarnings("unused") 90 | private static boolean typecheck(Class receiverClass, Object receiver) { 91 | return receiverClass == receiver.getClass(); 92 | } 93 | 94 | @SuppressWarnings("unused") 95 | private static MethodHandle find(HashMap, MethodHandle> map, Object o) { 96 | Class receiverClass = o.getClass(); 97 | MethodHandle target = map.get(receiverClass); 98 | if (target == null) { 99 | throw new IllegalStateException("no visitlet register for type " + receiverClass.getName()); 100 | } 101 | return target; 102 | } 103 | } -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/StringSwitchCallSite.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static com.github.forax.exotic.StringSwitch.NO_MATCH; 4 | import static java.lang.invoke.MethodHandles.constant; 5 | import static java.lang.invoke.MethodHandles.dropArguments; 6 | import static java.lang.invoke.MethodHandles.guardWithTest; 7 | import static java.lang.invoke.MethodHandles.insertArguments; 8 | import static java.lang.invoke.MethodType.methodType; 9 | 10 | import java.lang.invoke.MethodHandle; 11 | import java.lang.invoke.MethodHandles; 12 | import java.lang.invoke.MethodHandles.Lookup; 13 | import java.lang.invoke.MethodType; 14 | import java.lang.invoke.MutableCallSite; 15 | import java.util.HashMap; 16 | import java.util.Objects; 17 | 18 | class StringSwitchCallSite extends MutableCallSite { 19 | private static final MethodType STRING_TO_INT = methodType(int.class, String.class); 20 | private static final MethodHandle FALLBACK, EQUALS, GET_OR_DEFAULT, NULLCHECK; 21 | static { 22 | Lookup lookup = MethodHandles.lookup(); 23 | try { 24 | FALLBACK = lookup.findVirtual(StringSwitchCallSite.class, "fallback", STRING_TO_INT); 25 | EQUALS = lookup.findVirtual(String.class, "equals", methodType(boolean.class, Object.class)); 26 | MethodHandle get = lookup.findVirtual(HashMap.class, "getOrDefault", methodType(Object.class, Object.class, Object.class)); 27 | GET_OR_DEFAULT = MethodHandles.insertArguments(get, 2, -1).asType(methodType(int.class, HashMap.class, String.class)); 28 | MethodHandle nullCheck = lookup.findStatic(Objects.class, "isNull", methodType(boolean.class, Object.class)); 29 | NULLCHECK = nullCheck.asType(methodType(boolean.class, String.class)); 30 | } catch(NoSuchMethodException | IllegalAccessException e) { 31 | throw new AssertionError(e); 32 | } 33 | } 34 | 35 | private static final int MAX_DEPTH = 32; 36 | 37 | private final int depth; 38 | private final StringSwitchCallSite callsite; 39 | private final String[] stringcases; 40 | private final HashMap map; 41 | 42 | private StringSwitchCallSite(String[] stringcases, HashMap map) { 43 | super(STRING_TO_INT); 44 | this.depth = 0; 45 | this.callsite = this; 46 | this.stringcases = stringcases; 47 | this.map = map; 48 | setTarget(FALLBACK.bindTo(this)); 49 | } 50 | 51 | private StringSwitchCallSite(int depth, StringSwitchCallSite callsite, String[] stringcases, HashMap map) { 52 | super(STRING_TO_INT); 53 | this.depth = depth; 54 | this.callsite = callsite; 55 | this.stringcases = stringcases; 56 | this.map = map; 57 | setTarget(FALLBACK.bindTo(this)); 58 | } 59 | 60 | static StringSwitchCallSite create(String[] stringcases) { 61 | HashMap map = new HashMap<>(); 62 | for(int i = 0; i < stringcases.length; i++) { 63 | String stringcase = Objects.requireNonNull(stringcases[i]); 64 | if (map.put(stringcase, i) != null) { 65 | throw new IllegalStateException(stringcase + " value appear more than once"); 66 | } 67 | } 68 | return new StringSwitchCallSite(stringcases, map); 69 | } 70 | 71 | @SuppressWarnings("unused") 72 | private int fallback(String value) { 73 | Objects.requireNonNull(value); 74 | int index = map.getOrDefault(value, NO_MATCH); 75 | 76 | //System.out.println("depth " + depth); 77 | 78 | if (depth == MAX_DEPTH) { 79 | //System.out.println("reach max depth"); 80 | callsite.setTarget(GET_OR_DEFAULT.bindTo(map)); 81 | return index; 82 | } 83 | 84 | if (depth == stringcases.length) { 85 | //System.out.println("reach cases length"); 86 | callsite.setTarget(createCascadeIfEquals(stringcases)); 87 | return index; 88 | } 89 | 90 | setTarget(guardWithTest(insertArguments(EQUALS, 1, value), 91 | dropArguments(constant(int.class, index), 0, String.class), 92 | new StringSwitchCallSite(depth + 1, callsite, stringcases, map).dynamicInvoker())); 93 | return index; 94 | } 95 | 96 | private static MethodHandle createCascadeIfEquals(String[] stringcases) { 97 | MethodHandle target = dropArguments(constant(int.class, NO_MATCH), 0, String.class); 98 | for(int i = stringcases.length; --i >= 0;) { 99 | String stringcase = stringcases[i]; 100 | target = guardWithTest(insertArguments(EQUALS, 1, stringcase), 101 | dropArguments(constant(int.class, i), 0, String.class), 102 | target); 103 | } 104 | return target; 105 | } 106 | 107 | static MethodHandle wrapNullIfNecessary(boolean nullMatch, MethodHandle mh) { 108 | if (!nullMatch) { 109 | return mh; 110 | } 111 | return guardWithTest(NULLCHECK, 112 | dropArguments(constant(int.class, StringSwitch.NULL_MATCH), 0, String.class), 113 | mh); 114 | } 115 | } -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/perf/TypeSwitchBenchMark.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic.perf; 2 | 3 | import java.net.URI; 4 | import java.nio.CharBuffer; 5 | import java.time.LocalDate; 6 | import java.util.Date; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import org.openjdk.jmh.annotations.Benchmark; 10 | import org.openjdk.jmh.annotations.BenchmarkMode; 11 | import org.openjdk.jmh.annotations.Fork; 12 | import org.openjdk.jmh.annotations.Measurement; 13 | import org.openjdk.jmh.annotations.Mode; 14 | import org.openjdk.jmh.annotations.OutputTimeUnit; 15 | import org.openjdk.jmh.annotations.Scope; 16 | import org.openjdk.jmh.annotations.State; 17 | import org.openjdk.jmh.annotations.Warmup; 18 | import org.openjdk.jmh.runner.Runner; 19 | import org.openjdk.jmh.runner.RunnerException; 20 | import org.openjdk.jmh.runner.options.Options; 21 | import org.openjdk.jmh.runner.options.OptionsBuilder; 22 | 23 | import com.github.forax.exotic.TypeSwitch; 24 | 25 | @SuppressWarnings("static-method") 26 | @Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) 27 | @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) 28 | @Fork(3) 29 | @BenchmarkMode(Mode.AverageTime) 30 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 31 | @State(Scope.Benchmark) 32 | public class TypeSwitchBenchMark { 33 | 34 | interface I { /* empty */ } 35 | interface J { /* empty */ } 36 | static class A { /* empty */ } 37 | static class B implements I { /* empty */ } 38 | static class C implements J { /* empty */ } 39 | static class D implements I, J { /* empty */ } 40 | static class E implements I { /* empty */ } 41 | static class F implements J { /* empty */ } 42 | 43 | private static final TypeSwitch SMALL_TYPE_SWITCH = TypeSwitch.create(true, 44 | D.class, C.class, B.class, A.class/*, J.class, I.class*/); 45 | 46 | private static final TypeSwitch BIG_TYPE_SWITCH = TypeSwitch.create(true, 47 | D.class, C.class, B.class, A.class, J.class, I.class, 48 | String.class, StringBuilder.class, CharSequence.class, URI.class, LocalDate.class, Comparable.class, Object.class); 49 | 50 | private static final Object[] DATA = { 51 | new D(), new E(), new C(), new A(), new F(), new B(), new A(), new E(), new F(), new D() { /*empty*/}, new A() { /*empty*/ }, 52 | "hello", new StringBuilder("hello"), CharBuffer.wrap("hello"), 53 | LocalDate.now(), new Date(), A.class 54 | }; 55 | 56 | @Benchmark 57 | public int small_small_type_switch() { 58 | int sum = 0; 59 | for(int i = 0; i < 4; i++) { 60 | Object o = DATA[i]; 61 | sum += SMALL_TYPE_SWITCH.typeSwitch(o); 62 | } 63 | return sum; 64 | } 65 | 66 | @Benchmark 67 | public int small_small_instanceof_cascade() { 68 | int sum = 0; 69 | for(int i = 0; i < 4; i++) { 70 | Object o = DATA[i]; 71 | int value; 72 | if (o == null) { value = TypeSwitch.NULL_MATCH; } else 73 | if (o instanceof D) { value = 0; } else 74 | if (o instanceof C) { value = 1; } else 75 | if (o instanceof B) { value = 2; } else 76 | if (o instanceof A) { value = 3; } else 77 | { value = TypeSwitch.NO_MATCH; } 78 | sum += value; 79 | } 80 | return sum; 81 | } 82 | 83 | @Benchmark 84 | public int small_big_type_switch() { 85 | int sum = 0; 86 | for(Object o: DATA) { 87 | sum += SMALL_TYPE_SWITCH.typeSwitch(o); 88 | } 89 | return sum; 90 | } 91 | 92 | @Benchmark 93 | public int small_big_instanceof_cascade() { 94 | int sum = 0; 95 | for(Object o: DATA) { 96 | int value; 97 | if (o == null) { value = TypeSwitch.NULL_MATCH; } else 98 | if (o instanceof D) { value = 0; } else 99 | if (o instanceof C) { value = 1; } else 100 | if (o instanceof B) { value = 2; } else 101 | if (o instanceof A) { value = 3; } else 102 | { value = TypeSwitch.NO_MATCH; } 103 | sum += value; 104 | } 105 | return sum; 106 | } 107 | 108 | @Benchmark 109 | public int big_big_type_switch() { 110 | int sum = 0; 111 | for(Object o: DATA) { 112 | sum += BIG_TYPE_SWITCH.typeSwitch(o); 113 | } 114 | return sum; 115 | } 116 | 117 | @Benchmark 118 | public int big_big_instanceof_cascade() { 119 | int sum = 0; 120 | for(Object o: DATA) { 121 | int value; 122 | if (o == null) { value = TypeSwitch.NULL_MATCH; } else 123 | if (o instanceof D) { value = 0; } else 124 | if (o instanceof C) { value = 1; } else 125 | if (o instanceof B) { value = 2; } else 126 | if (o instanceof A) { value = 3; } else 127 | if (o instanceof J) { value = 4; } else 128 | if (o instanceof I) { value = 5; } else 129 | if (o instanceof String) { value = 6; } else 130 | if (o instanceof StringBuilder) { value = 7; } else 131 | if (o instanceof CharSequence) { value = 8; } else 132 | if (o instanceof URI) { value = 9; } else 133 | if (o instanceof LocalDate) { value = 10; } else 134 | if (o instanceof Comparable) { value = 11; } else 135 | if (o instanceof Object) { value = 12; } else 136 | { value = TypeSwitch.NO_MATCH; } 137 | sum += value; 138 | } 139 | return sum; 140 | } 141 | 142 | public static void main(String[] args) throws RunnerException { 143 | Options opt = new OptionsBuilder().include(TypeSwitchBenchMark.class.getName()).build(); 144 | new Runner(opt).run(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/StructuralCallTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static java.lang.invoke.MethodHandles.publicLookup; 5 | import static java.lang.invoke.MethodType.methodType; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import java.time.LocalTime; 10 | import java.util.List; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | @SuppressWarnings("static-method") 15 | public class StructuralCallTests { 16 | @Test 17 | public void simple() { 18 | StructuralCall call = StructuralCall.create(lookup(), "toString", methodType(String.class)); 19 | assertEquals("mirror", call.invoke("mirror")); 20 | assertEquals("14", call.invoke(14)); 21 | assertEquals("5.0", call.invoke(5.0)); 22 | } 23 | 24 | @Test 25 | public void comparable() { 26 | StructuralCall call = 27 | StructuralCall.create(lookup(), "compareTo", methodType(int.class, Object.class)); 28 | assertEquals(0, (int) call.invoke("foo", "foo")); 29 | assertEquals(0, (int) call.invoke(14, 14)); 30 | assertEquals(0, (int) call.invoke(LocalTime.of(14, 12), LocalTime.of(14, 12))); 31 | } 32 | 33 | @Test 34 | public void wrongConfiguration() { 35 | assertThrows( 36 | NullPointerException.class, 37 | () -> StructuralCall.create(null, "foo", methodType(void.class))); 38 | assertThrows( 39 | NullPointerException.class, 40 | () -> StructuralCall.create(lookup(), null, methodType(void.class))); 41 | assertThrows(NullPointerException.class, () -> StructuralCall.create(lookup(), "foo", null)); 42 | } 43 | 44 | 45 | 46 | @Test 47 | public void cannotAccessToAPrivateMethod() { 48 | StructuralCall call = 49 | StructuralCall.create(lookup(), "m", methodType(String.class, String.class)); 50 | assertThrows(IllegalAccessError.class, () -> call.invoke(new com.github.forax.exotic.noaccess.NoAccess(), "test")); 51 | } 52 | 53 | static class WrongLookup { 54 | long m(double d) { 55 | return (long) d; 56 | } 57 | } 58 | 59 | @Test 60 | public void publicLookupCanNotAccessPackageMethod() { 61 | StructuralCall call = 62 | StructuralCall.create(publicLookup(), "m", methodType(long.class, double.class)); 63 | assertThrows(IllegalAccessError.class, () -> call.invoke(new WrongLookup(), 4.0)); 64 | } 65 | 66 | static class NotFound { 67 | /* empty */ 68 | } 69 | 70 | @Test 71 | public void noMethodDefined() { 72 | StructuralCall call = 73 | StructuralCall.create(lookup(), "m", methodType(String.class, String.class)); 74 | assertThrows(NoSuchMethodError.class, () -> call.invoke(new NotFound(), "whereAreYou")); 75 | } 76 | 77 | @Test 78 | public void accessMethodThroughInterface() { 79 | StructuralCall call = StructuralCall.create(lookup(), "isEmpty", methodType(boolean.class)); 80 | assertEquals(false, (boolean) call.invoke(List.of(1, 2, 3))); 81 | } 82 | 83 | @SuppressWarnings("unused") 84 | static class WrongParameters { 85 | void m(boolean b) { 86 | /* empty */ 87 | } 88 | 89 | void m(int i) { 90 | /* empty */ 91 | } 92 | 93 | void m(double d) { 94 | /* empty */ 95 | } 96 | } 97 | 98 | @Test 99 | public void callingAMethodWithTheWrongClass() { 100 | WrongParameters wrongParameters = new WrongParameters(); 101 | StructuralCall call1 = 102 | StructuralCall.create(lookup(), "m", methodType(void.class, boolean.class)); 103 | assertThrows(ClassCastException.class, () -> call1.invoke(wrongParameters, "oops")); 104 | StructuralCall call2 = StructuralCall.create(lookup(), "m", methodType(void.class, int.class)); 105 | assertThrows(ClassCastException.class, () -> call2.invoke(wrongParameters, "oops")); 106 | StructuralCall call3 = 107 | StructuralCall.create(lookup(), "m", methodType(void.class, double.class)); 108 | assertThrows(ClassCastException.class, () -> call3.invoke(wrongParameters, "oops")); 109 | } 110 | 111 | static class WrongNumberOfArguments { 112 | long m(int i, long l) { 113 | return i + l; 114 | } 115 | } 116 | 117 | @Test 118 | public void callingAMethodWithTheWrongNumberOfArguments() { 119 | WrongNumberOfArguments wrongNumberOfArguments = new WrongNumberOfArguments(); 120 | StructuralCall call = 121 | StructuralCall.create( 122 | lookup(), "m", methodType(long.class, int.class, long.class)); // 2 parameters 123 | assertThrows( 124 | IllegalArgumentException.class, () -> call.invoke(wrongNumberOfArguments)); // 0 argument 125 | assertThrows( 126 | IllegalArgumentException.class, () -> call.invoke(wrongNumberOfArguments, 0)); // 1 argument 127 | assertThrows( 128 | IllegalArgumentException.class, 129 | () -> call.invoke(wrongNumberOfArguments, 0, 0, 0)); // 3 argument 130 | assertThrows( 131 | IllegalArgumentException.class, 132 | () -> call.invoke(wrongNumberOfArguments, 0, 0, 0, 0)); // 4 argument 133 | assertThrows( 134 | IllegalArgumentException.class, 135 | () -> call.invoke(wrongNumberOfArguments, 0, 0, 0, 0, 0)); // 5 argument 136 | assertThrows( 137 | IllegalArgumentException.class, 138 | () -> call.invoke(wrongNumberOfArguments, 0, 0, 0, 0, 0, 0)); // 6 argument 139 | assertThrows( 140 | IllegalArgumentException.class, 141 | () -> call.invoke(wrongNumberOfArguments, 0, 0, 0, 0, 0, 0, 0)); // 7 argument 142 | assertThrows( 143 | IllegalArgumentException.class, 144 | () -> call.invoke(wrongNumberOfArguments, 0, 0, 0, 0, 0, 0, 0, 0)); // 8 argument 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/ObjectSupport.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import java.io.Serializable; 4 | import java.lang.invoke.MethodHandles.Lookup; 5 | import java.lang.reflect.Field; 6 | import java.util.function.Function; 7 | 8 | /** 9 | * Provide a fast implementation for {@link Object#equals(Object)} and {@link Object#hashCode()}. 10 | *

11 | * An {@code ObjectSupport} can be created either from a {@link Lookup}, the class containing the fields 12 | * and a set of lambdas accessing the fields using {@link ObjectSupport#of(Lookup, Class, ProjectionFunction...)} 13 | * or from a {@link Lookup}, the class containing the fields and a set of field names using 14 | * {@link ObjectSupport#of(Lookup, Class, String...)}. 15 | *

16 | * The following example shows how to create and use a {@code ObjectSupport} configured 17 | * to use the fields {@code name} and {@code age}. 18 | *

 19 |  * class Person {
 20 |  *   private static final ObjectSupport<Person> SUPPORT = SUPPORT = ObjectSupport.of(lookup(), Person.class, p -> p.name, p -> p.age);
 21 |  *  
 22 |  *   private String name;
 23 |  *   private int age;
 24 |  *
 25 |  *   public Person(String name, int age) {
 26 |  *     this.name = name;
 27 |  *     this.age = age;
 28 |  *   }
 29 |  *   
 30 |  *   public boolean equals(Object other) {
 31 |  *     return SUPPORT.equals(this, other);
 32 |  *   }
 33 |  *   
 34 |  *   public int hashCode() {
 35 |  *     return SUPPORT.hashCode(this);
 36 |  *   }
 37 |  * }
 38 |  * 
39 | * 40 | * @param the type of the class. 41 | */ 42 | public interface ObjectSupport { 43 | /** 44 | * Test if two object are equals. 45 | * 46 | * @param self an instance of the class used to create the current {@code ObjectSupport}. 47 | * @param other any instance or null. 48 | * @return true if the two objects are equals. 49 | * @throws NullPointerException if {@code self} is null. 50 | * @throws ClassCastException if {@code self} is not an instance of the class 51 | * used to create the current {@code ObjectSupport}. 52 | * @see Object#equals(Object) 53 | */ 54 | public abstract boolean equals(T self, Object other); 55 | 56 | /** 57 | * Return a hash value of an instance of the class used to create the current {@code ObjectSupport}. 58 | * 59 | * @param self an instance of the class used to create the current {@code ObjectSupport}. 60 | * @return a hash value of {@code self}. 61 | * @throws NullPointerException if {@code self} is null. 62 | * @throws ClassCastException if {@code self} is not an instance of the class 63 | * used to create the current {@code ObjectSupport}. 64 | * @see Object#hashCode() 65 | */ 66 | public abstract int hashCode(T self); 67 | 68 | /** 69 | * Return an object support from a lookup object and some field names. 70 | * 71 | * @param the type of the class. 72 | * @param lookup a lookup with enough access rights to see the class fields. 73 | * @param type the class containing the fields. 74 | * @param fieldNames names of the fields that will be use for the computations. 75 | * @return a new fresh object support. This object should always be stored in a {@code static} {@code final} field. 76 | * @throws NullPointerException if {@code lookup} is null, {@code type} is null or the array of field is null. 77 | */ 78 | public static ObjectSupport of(Lookup lookup, Class type, String... fieldNames) { 79 | return ObjectSupports.createUsingFieldNames(lookup, type, fieldNames); 80 | } 81 | 82 | /** 83 | * Return an object support from a lookup object and a function that does reflection to find the fields. 84 | * 85 | * @param the type of the class. 86 | * @param lookup a lookup with enough access rights to see the class fields. 87 | * @param type the class containing the fields. 88 | * @param transformer a function that map the class to the fields used for the subsequent computations. 89 | * @return a new fresh object support. This object should always be stored in a {@code static} {@code final} field. 90 | * @throws NullPointerException if {@code lookup} is null, {@code type} is null or {@code transformer} is null. 91 | */ 92 | public static ObjectSupport ofReflection(Lookup lookup, Class type, Function, ? extends Field[]> transformer) { 93 | return ObjectSupports.createUsingReflectFields(lookup, type, transformer); 94 | } 95 | 96 | /** 97 | * A function that retrieve the value of a field of the class. 98 | * 99 | * @param type of the parameter. 100 | * @param type of the return value. 101 | * 102 | * @see ObjectSupport#of(Lookup, Class, ProjectionFunction...) 103 | */ 104 | @FunctionalInterface 105 | public interface ProjectionFunction extends Function, Serializable { 106 | // empty 107 | } 108 | 109 | /** 110 | * Return an object support from a lookup object and lambdas returning the fields. 111 | * 112 | * @param the type of the class. 113 | * @param lookup a lookup with enough access rights to see the class fields. 114 | * @param type the class containing the fields. 115 | * @param projections lambdas that returns the value of a field of the class. 116 | * @return a new fresh object support. This object should always be stored in a {@code static} {@code final} field. 117 | * @throws NullPointerException if {@code lookup} is null, {@code type} is null or {@code transformer} is null. 118 | * @throws IllegalArgumentException if the code of the lambdas is not accessible from the lookup object or 119 | * if one lambda doesn't do a field access. 120 | */ 121 | @SafeVarargs 122 | public static ObjectSupport of(Lookup lookup, Class type, ProjectionFunction... projections) { 123 | return ObjectSupports.createUsingLambdas(lookup, type, projections); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exotic [![Continuous Integration](https://github.com/forax/exotic/actions/workflows/main.yml/badge.svg)](https://github.com/forax/exotic/actions/workflows/main.yml) 2 | A bestiary of classes implementing exotic semantics in Java 3 | 4 | In Java, a static final field is considered as a constant by the virtual machine, 5 | but a final field of an object which is a constant is not itself considered as a constant. 6 | Exotic allows to see a constant's field as a constant, a result of a calculation as a constant, 7 | to change at runtime the value of a constant, etc. 8 | 9 | This library run on Java 8+ and is fully compatible with Java 9 modules. 10 | 11 | This library needs Java 11+ to be built. 12 | 13 | ### MostlyConstant - [javadoc](https://jitpack.io/com/github/forax/exotic/master/javadoc/com/github/forax/exotic/MostlyConstant.html) 14 | 15 | A constant for the VM that can be changed by de-optimizing all the codes that contain the previous value of the constant. 16 | 17 | ```java 18 | private static final MostlyConstant FOO = new MostlyConstant<>(42, int.class); 19 | private static final IntSupplier FOO_GETTER = FOO.intGetter(); 20 | 21 | public static int getFoo() { 22 | return FOO_GETTER.getAsInt(); 23 | } 24 | public static void setFoo(int value) { 25 | FOO.setAndDeoptimize(value); 26 | } 27 | ``` 28 | 29 | ### StableField - [javadoc](https://jitpack.io/com/github/forax/exotic/master/javadoc/com/github/forax/exotic/StableField.html) 30 | 31 | A field that becomes a constant if the object itself is constant and the field is initialized 32 | 33 | ```java 34 | enum Option { 35 | a, b; 36 | 37 | private static final Function UPPERCASE = 38 | StableField.getter(lookup(), Option.class, "uppercase", String.class); 39 | 40 | private String uppercase; // stable 41 | 42 | public String upperCase() { 43 | String uppercase = UPPERCASE.apply(this); 44 | if (uppercase != null) { 45 | return uppercase; 46 | } 47 | return this.uppercase = name().toUpperCase(); 48 | } 49 | } 50 | ... 51 | Option.a.upperCase() // constant "A" 52 | ``` 53 | 54 | ### ConstantMemoizer - [javadoc](https://jitpack.io/com/github/forax/exotic/master/javadoc/com/github/forax/exotic/ConstantMemoizer.html) 55 | 56 | A function that returns a constant value if its parameter is a constant. 57 | 58 | ```java 59 | private static final ToIntFunction MEMOIZER = 60 | ConstantMemoizer.intMemoizer(Level::ordinal, Level.class); 61 | ... 62 | MEMOIZER.applyAsInt("foo") // constant 3 63 | ``` 64 | 65 | ### ObjectSupport - [javadoc](https://jitpack.io/com/github/forax/exotic/master/javadoc/com/github/forax/exotic/ObjectSupport.html) 66 | 67 | Provide a fast implementation for equals() and hashCode(). 68 | 69 | ```java 70 | class Person { 71 | private static final ObjectSupport SUPPORT = 72 | ObjectSupport.of(lookup(), Person.class, p -> p.name); 73 | 74 | private String name; 75 | ... 76 | 77 | public boolean equals(Object other) { 78 | return SUPPORT.equals(this, other); 79 | } 80 | 81 | public int hashCode() { 82 | return SUPPORT.hashCode(this); 83 | } 84 | } 85 | ``` 86 | 87 | ### StructuralCall - [javadoc](https://jitpack.io/com/github/forax/exotic/master/javadoc/com/github/forax/exotic/StructuralCall.html) 88 | 89 | A method call that can call different method implementations if they share the same name and same parameter types. 90 | 91 | ```java 92 | private static final StructuralCall IS_EMPTY = 93 | StructuralCall.create(lookup(), "isEmpty", methodType(boolean.class)); 94 | 95 | static boolean isEmpty(Object o) { // can be called with a Map, a Collection or a String 96 | return IS_EMPTY.invoke(o); 97 | } 98 | ``` 99 | 100 | ### Visitor - [javadoc](https://jitpack.io/com/github/forax/exotic/master/javadoc/com/github/forax/exotic/Visitor.html) 101 | 102 | An open visitor, a visitor that does allow new types and new operations, can be implemented using a Map 103 | that associates a class to a lambda, but this implementation loose inlining thus perform badly compared to the Gof Visitor. 104 | This class implements an open visitor that's used inlining caches. 105 | 106 | ```java 107 | private static final Visitor VISITOR = 108 | Visitor.create(Void.class, int.class, opt -> opt 109 | .register(Value.class, (v, value, __) -> value.value) 110 | .register(Add.class, (v, add, __) -> v.visit(add.left, null) + v.visit(add.right, null)) 111 | ); 112 | ... 113 | Expr expr = new Add(new Add(new Value(7), new Value(10)), new Value(4)); 114 | int value = VISITOR.visit(expr, null); // 21 115 | ``` 116 | 117 | ### TypeSwitch - [javadoc](https://jitpack.io/com/github/forax/exotic/master/javadoc/com/github/forax/exotic/TypeSwitch.html) 118 | 119 | Express a switch on type as function from an object to an index + a classical switch on the possible indexes. 120 | The TypeSwitch should be more efficient than a cascade of if instanceof. 121 | 122 | ```java 123 | private static final TypeSwitch TYPE_SWITCH = TypeSwitch.create(true, Integer.class, String.class); 124 | 125 | public static String asString(Object o) { 126 | switch(TYPE_SWITCH.typeSwitch(o)) { 127 | case TypeSwitch.NULL_MATCH: 128 | return "null"; 129 | case 0: 130 | return "Integer"; 131 | case 1: 132 | return "String"; 133 | default: // TypeSwitch.NO_MATCH 134 | return "unknown"; 135 | } 136 | } 137 | ``` 138 | 139 | 140 | ## Build Tool Integration [![](https://jitpack.io/v/forax/exotic.svg)](https://jitpack.io/#forax/exotic) 141 | 142 | Get latest binary distribution via [JitPack](https://jitpack.io/#forax/exotic) 143 | 144 | 145 | ### Maven 146 | 147 | 148 | 149 | jitpack.io 150 | https://jitpack.io 151 | 152 | 153 | 154 | com.github.forax 155 | exotic 156 | master-SNAPSHOT 157 | 158 | 159 | 160 | ### Gradle 161 | 162 | repositories { 163 | ... 164 | maven { url 'https://jitpack.io' } 165 | } 166 | dependencies { 167 | compile 'com.github.forax:exotic:master-SNAPSHOT' 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/MostlyConstantTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import java.util.function.DoubleSupplier; 7 | import java.util.function.IntSupplier; 8 | import java.util.function.LongSupplier; 9 | import java.util.function.Supplier; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | @SuppressWarnings("static-method") 14 | public class MostlyConstantTests { 15 | static class ObjectSandbox1 { 16 | static final MostlyConstant VALUE = new MostlyConstant<>("hello", String.class); 17 | static final Supplier VALUE_GETTER = VALUE.getter(); 18 | } 19 | 20 | @Test 21 | public void testObjectSimpleChange() { 22 | assertEquals("hello", ObjectSandbox1.VALUE_GETTER.get()); 23 | ObjectSandbox1.VALUE.setAndDeoptimize("hell"); 24 | assertEquals("hell", ObjectSandbox1.VALUE_GETTER.get()); 25 | } 26 | 27 | static class ObjectSandbox2 { 28 | static final MostlyConstant VALUE = new MostlyConstant<>("hello", String.class); 29 | static final Supplier VALUE_GETTER = VALUE.getter(); 30 | } 31 | 32 | @Test 33 | public void testObjectSimpleChangeOptimized() { 34 | class Fake { 35 | String test() { 36 | return ObjectSandbox2.VALUE_GETTER.get(); 37 | } 38 | } 39 | 40 | Fake fake = new Fake(); 41 | for (int i = 0; i < 1_000_000; i++) { 42 | assertEquals("hello", fake.test()); 43 | } 44 | assertEquals("hello", fake.test()); 45 | 46 | ObjectSandbox2.VALUE.setAndDeoptimize("hell"); 47 | assertEquals("hell", fake.test()); 48 | } 49 | 50 | static class IntSandbox1 { 51 | static final MostlyConstant VALUE = new MostlyConstant<>(42, int.class); 52 | static final IntSupplier VALUE_GETTER = VALUE.intGetter(); 53 | } 54 | 55 | @Test 56 | public void testIntSimpleChange() { 57 | assertEquals(42, IntSandbox1.VALUE_GETTER.getAsInt()); 58 | IntSandbox1.VALUE.setAndDeoptimize(43); 59 | assertEquals(43, IntSandbox1.VALUE_GETTER.getAsInt()); 60 | } 61 | 62 | static class IntSandbox2 { 63 | static final MostlyConstant VALUE = new MostlyConstant<>(42, int.class); 64 | static final IntSupplier VALUE_GETTER = VALUE.intGetter(); 65 | } 66 | 67 | @Test 68 | public void testIntSimpleChangeOptimized() { 69 | class Fake { 70 | int test() { 71 | return IntSandbox2.VALUE_GETTER.getAsInt(); 72 | } 73 | } 74 | 75 | Fake fake = new Fake(); 76 | for (int i = 0; i < 1_000_000; i++) { 77 | assertEquals(42, fake.test()); 78 | } 79 | assertEquals(42, fake.test()); 80 | 81 | IntSandbox2.VALUE.setAndDeoptimize(43); 82 | assertEquals(43, fake.test()); 83 | } 84 | 85 | static class LongSandbox1 { 86 | static final MostlyConstant VALUE = new MostlyConstant<>(42L, long.class); 87 | static final LongSupplier VALUE_GETTER = VALUE.longGetter(); 88 | } 89 | 90 | @Test 91 | public void testLongSimpleChange() { 92 | assertEquals(42, LongSandbox1.VALUE_GETTER.getAsLong()); 93 | LongSandbox1.VALUE.setAndDeoptimize(43L); 94 | assertEquals(43, LongSandbox1.VALUE_GETTER.getAsLong()); 95 | } 96 | 97 | static class LongSandbox2 { 98 | static final MostlyConstant VALUE = new MostlyConstant<>(42L, long.class); 99 | static final LongSupplier VALUE_GETTER = VALUE.longGetter(); 100 | } 101 | 102 | @Test 103 | public void testLongSimpleChangeOptimized() { 104 | class Fake { 105 | long test() { 106 | return LongSandbox2.VALUE_GETTER.getAsLong(); 107 | } 108 | } 109 | 110 | Fake fake = new Fake(); 111 | for (int i = 0; i < 1_000_000; i++) { 112 | assertEquals(42L, fake.test()); 113 | } 114 | assertEquals(42L, fake.test()); 115 | 116 | LongSandbox2.VALUE.setAndDeoptimize(43L); 117 | assertEquals(43L, fake.test()); 118 | } 119 | 120 | static class DoubleSandbox1 { 121 | static final MostlyConstant VALUE = new MostlyConstant<>(42.0, double.class); 122 | static final DoubleSupplier VALUE_GETTER = VALUE.doubleGetter(); 123 | } 124 | 125 | @Test 126 | public void testDoubleSimpleChange() { 127 | assertEquals(42.0, DoubleSandbox1.VALUE_GETTER.getAsDouble()); 128 | DoubleSandbox1.VALUE.setAndDeoptimize(43.0); 129 | assertEquals(43, DoubleSandbox1.VALUE_GETTER.getAsDouble()); 130 | } 131 | 132 | static class DoubleSandbox2 { 133 | static final MostlyConstant VALUE = new MostlyConstant<>(42.0, double.class); 134 | static final DoubleSupplier VALUE_GETTER = VALUE.doubleGetter(); 135 | } 136 | 137 | @Test 138 | public void testDoubleSimpleChangeOptimized() { 139 | class Fake { 140 | double test() { 141 | return DoubleSandbox2.VALUE_GETTER.getAsDouble(); 142 | } 143 | } 144 | 145 | Fake fake = new Fake(); 146 | for (int i = 0; i < 1_000_000; i++) { 147 | assertEquals(42L, fake.test()); 148 | } 149 | assertEquals(42L, fake.test()); 150 | 151 | DoubleSandbox2.VALUE.setAndDeoptimize(43.0); 152 | assertEquals(43L, fake.test()); 153 | } 154 | 155 | @Test 156 | public void testConstructorWithVoidType() { 157 | assertThrows(IllegalArgumentException.class, () -> new MostlyConstant<>(null, void.class)); 158 | } 159 | 160 | @Test 161 | public void testConstructorWithNullType() { 162 | assertThrows(NullPointerException.class, () -> new MostlyConstant<>(null, null)); 163 | } 164 | 165 | @Test 166 | public void testSpecializedGettersWithWrapperTypes() { 167 | assertThrows( 168 | IllegalStateException.class, () -> new MostlyConstant<>(0, Integer.class).intGetter()); 169 | assertThrows( 170 | IllegalStateException.class, () -> new MostlyConstant<>(0L, Long.class).longGetter()); 171 | assertThrows( 172 | IllegalStateException.class, () -> new MostlyConstant<>(0.0, Double.class).doubleGetter()); 173 | 174 | assertThrows( 175 | IllegalStateException.class, () -> new MostlyConstant<>(0, Object.class).intGetter()); 176 | assertThrows( 177 | IllegalStateException.class, () -> new MostlyConstant<>(0L, Object.class).longGetter()); 178 | assertThrows( 179 | IllegalStateException.class, () -> new MostlyConstant<>(0.0, Object.class).doubleGetter()); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/Visitor.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.insertArguments; 4 | 5 | import java.lang.invoke.MethodHandle; 6 | import java.lang.invoke.MethodType; 7 | import java.util.HashMap; 8 | import java.util.Objects; 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | * A open visitor based on lambdas that tries hard to optimize all intra-visitor calls. 13 | * 14 | * The visitor let you ({@link Registry#register(Class, Visitlet)}) a computation for any type you want 15 | * (they do not have to have a root interface by example) and then when {@link Visitor#visit(Object, Object) visiting} 16 | * the visitor, the right computation is called depending on the class of the expression. 17 | * 18 | * The idea of the implementation is to send an object that has the same interface as the Visitor as first parameter of each lambda 19 | * that implement an inlining cache specific for this lambda, thus mimicking the inlining caches that 20 | * you naturally have when implementing the double-dispatch (the visitor pattern of the Gof). 21 | * 22 | * Let suppose we have the following abstract syntax tree, 23 | *
 24 |  * interface Expr { }
 25 |  * class Value implements Expr { final int value; Value(int value) { this.value = value; }}
 26 |  * class Add implements Expr { final Expr left, right; Add(Expr left, Expr right) { this.left = left; this.right = right; }}
 27 |  * 
28 | * 29 | * to evaluate it, we can use a visitor configured like this 30 | *
 31 |  *   private static final Visitor<Void, Integer> VISITOR = Visitor.create(Void.class, int.class, opt -> opt
 32 |  *       .register(Value.class, (visitor, value, __) -> value.value)
 33 |  *       .register(Add.class,   (visitor, add, __)   -> visitor.visit(add.left, null) + visitor.visit(add.right, null))
 34 |  *       );
 35 |  *   ...
 36 |  *   Expr expr = new Add(new Add(new Value(7), new Value(10)), new Value(4));
 37 |  *   int value = VISITOR.visit(expr, null);  // 21
 38 |  * 
39 | * 40 | * @param

type of the parameter value (the inherited attribute) 41 | * @param type of the return value (the synthesized attribute) 42 | */ 43 | @FunctionalInterface 44 | public interface Visitor { 45 | /** 46 | * Visit one of the {@link Visitlet} depending on the class of the expression {@code expr}. 47 | * 48 | * @param expr an expression 49 | * @param parameter a parameter or null. 50 | * @return the return value of the called {@link Visitlet}. 51 | * @throws NullPointerException if {@code expr} is null. 52 | * @throws IllegalStateException if the expression class has no corresponding visitlet defined 53 | */ 54 | R visit(Object expr, P parameter); 55 | 56 | /** 57 | * A computation part of a visitor specific for a type. 58 | * 59 | * @param the type of the expression. 60 | * @param

the type of the parameter, can be Void if the parameter is null. 61 | * @param the type of the return value. 62 | * 63 | * @see Registry#register(Class, Visitlet) 64 | * @see Visitor#visit(Object, Object) 65 | */ 66 | @FunctionalInterface 67 | interface Visitlet { 68 | /** 69 | * The computation for a part of an expression. 70 | * 71 | * @param visitor a visitor that can be called to do a recursive computation. 72 | * @param expr an expression. 73 | * @param parameter the value of a parameter or null. 74 | * @return the value of the computation. 75 | */ 76 | R visit(Visitor visitor, T expr, P parameter); 77 | } 78 | 79 | /** 80 | * Registry that contains the association between a type and its corresponding computation as a {@link Visitlet}. 81 | * 82 | * @param

the type of the parameter, can be Void if the parameter is null. 83 | * @param the type of the return value. 84 | */ 85 | interface Registry { 86 | /** 87 | * Register a computation for a specific type. 88 | * 89 | * @param type of the expression. 90 | * @param type the class of the expression that will be computed by the computation. 91 | * @param visitlet a computation. 92 | * @return itself so calls to register can be chained (as a build). 93 | * @throws NullPointerException if the {@code type} or {@code visitlet} is null. 94 | * @throws IllegalStateException if a computation has already register for a type. 95 | */ 96 | Registry register(Class type, Visitlet visitlet); 97 | } 98 | 99 | /** 100 | * Creates a visitor with the {@link Visitlet visitlets} registered in the {@link Registry}. 101 | * 102 | * @param

type of the parameter, can be Void if the parameter is null. 103 | * @param type of the return value. 104 | * @param pType class of the parameter type. 105 | * @param rType class of the return type. 106 | * @param consumer consumer that will register the {@link Visitlet visitlet} in the {@link Registry}. 107 | * @return a visitor configured with the {@link Visitlet visitlets}. 108 | * @throws NullPointerException if {@code pType}, {@code rType} or {@code consumer} is null. 109 | */ 110 | static Visitor create(Class

pType, Class rType, Consumer> consumer) { 111 | Objects.requireNonNull(pType); 112 | Objects.requireNonNull(rType); 113 | Objects.requireNonNull(consumer); 114 | HashMap, MethodHandle> map = new HashMap<>(); 115 | 116 | MethodType methodType = MethodType.methodType(rType, Object.class, pType); 117 | consumer.accept(new Registry() { 118 | @Override 119 | public Registry register(Class type, Visitlet visitlet) { 120 | Objects.requireNonNull(type); 121 | Objects.requireNonNull(visitlet); 122 | if (map.containsKey(type)) { 123 | throw new IllegalStateException("there is already a visitlet register for type " + type.getName()); 124 | } 125 | MethodHandle mh = insertArguments(VisitorCallSite.VISIT, 0, visitlet, VisitorCallSite.visitor(methodType, map)) 126 | .asType(methodType.changeParameterType(0, type)) 127 | .asType(methodType); 128 | map.put(type, mh); 129 | return this; 130 | } 131 | }); 132 | return VisitorCallSite.visitor(methodType, map); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/ConstantMemoizerTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import java.util.function.Function; 7 | import java.util.function.ToDoubleFunction; 8 | import java.util.function.ToIntFunction; 9 | import java.util.function.ToLongFunction; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | @SuppressWarnings("static-method") 14 | public class ConstantMemoizerTests { 15 | private static final Function OBJECT_FIBO = 16 | ConstantMemoizer.memoizer(n -> objectFibo(n), int.class, int.class); 17 | 18 | private static int objectFibo(int n) { 19 | if (n < 2) { 20 | return 1; 21 | } 22 | return OBJECT_FIBO.apply(n - 2) + OBJECT_FIBO.apply(n - 1); 23 | } 24 | 25 | @Test 26 | public void testObjectRecursive() { 27 | assertEquals(21, (int) OBJECT_FIBO.apply(7)); 28 | } 29 | 30 | private static final ToIntFunction INT_FIBO = 31 | ConstantMemoizer.intMemoizer(n -> intFibo(n), int.class); 32 | 33 | private static int intFibo(int n) { 34 | if (n < 2) { 35 | return 1; 36 | } 37 | return INT_FIBO.applyAsInt(n - 2) + INT_FIBO.applyAsInt(n - 1); 38 | } 39 | 40 | @Test 41 | public void testIntRecursive() { 42 | assertEquals(21, INT_FIBO.applyAsInt(7)); 43 | } 44 | 45 | private static final ToLongFunction LONG_FIBO = 46 | ConstantMemoizer.longMemoizer(n -> longFibo(n), int.class); 47 | 48 | private static long longFibo(int n) { 49 | if (n < 2) { 50 | return 1L; 51 | } 52 | return LONG_FIBO.applyAsLong(n - 2) + LONG_FIBO.applyAsLong(n - 1); 53 | } 54 | 55 | @Test 56 | public void testLongRecursive() { 57 | assertEquals(21L, LONG_FIBO.applyAsLong(7)); 58 | } 59 | 60 | private static final ToDoubleFunction DOUBLE_FIBO = 61 | ConstantMemoizer.doubleMemoizer(n -> doubleFibo(n), int.class); 62 | 63 | private static double doubleFibo(int n) { 64 | if (n < 2) { 65 | return 1.0; 66 | } 67 | return DOUBLE_FIBO.applyAsDouble(n - 2) + DOUBLE_FIBO.applyAsDouble(n - 1); 68 | } 69 | 70 | @Test 71 | public void testDoubleRecursive() { 72 | assertEquals(21.0, DOUBLE_FIBO.applyAsDouble(7)); 73 | } 74 | 75 | @Test 76 | public void testObjectSimple() { 77 | Function parseInt = 78 | ConstantMemoizer.memoizer(Integer::parseInt, String.class, int.class); 79 | assertEquals(666, (int) parseInt.apply("666")); 80 | assertEquals(666, (int) parseInt.apply("666")); 81 | } 82 | 83 | @Test 84 | public void testIntSimple() { 85 | ToIntFunction parseInt = ConstantMemoizer.intMemoizer(Integer::parseInt, String.class); 86 | assertEquals(777, parseInt.applyAsInt("777")); 87 | assertEquals(777, parseInt.applyAsInt("777")); 88 | } 89 | 90 | @Test 91 | public void testLongSimple() { 92 | ToLongFunction parseLong = ConstantMemoizer.longMemoizer(Long::parseLong, String.class); 93 | assertEquals(888L, parseLong.applyAsLong("888")); 94 | assertEquals(888L, parseLong.applyAsLong("888")); 95 | } 96 | 97 | @Test 98 | public void testDoubleSimple() { 99 | ToDoubleFunction parseLong = 100 | ConstantMemoizer.doubleMemoizer(Double::parseDouble, String.class); 101 | assertEquals(999.0, parseLong.applyAsDouble("999")); 102 | assertEquals(999.0, parseLong.applyAsDouble("999")); 103 | } 104 | 105 | @Test 106 | public void testObjectArgumentNull() { 107 | Function fun = 108 | ConstantMemoizer.memoizer(x -> x, Integer.class, Integer.class); 109 | assertThrows(NullPointerException.class, () -> fun.apply(null)); 110 | } 111 | 112 | @Test 113 | public void testIntArgumentNull() { 114 | ToIntFunction fun = ConstantMemoizer.intMemoizer(x -> x, Integer.class); 115 | assertThrows(NullPointerException.class, () -> fun.applyAsInt(null)); 116 | } 117 | 118 | @Test 119 | public void testLongArgumentNull() { 120 | ToLongFunction fun = ConstantMemoizer.longMemoizer(x -> x, Integer.class); 121 | assertThrows(NullPointerException.class, () -> fun.applyAsLong(null)); 122 | } 123 | 124 | @Test 125 | public void testDoubleArgumentNull() { 126 | ToDoubleFunction fun = ConstantMemoizer.doubleMemoizer(x -> x, Integer.class); 127 | assertThrows(NullPointerException.class, () -> fun.applyAsDouble(null)); 128 | } 129 | 130 | @Test 131 | public void testObjectReturnValueNull() { 132 | Function fun = 133 | ConstantMemoizer.memoizer(x -> null, Integer.class, Integer.class); 134 | assertThrows(NullPointerException.class, () -> fun.apply(3)); 135 | } 136 | 137 | @Test 138 | public void testWrongReturnType() { 139 | @SuppressWarnings("unchecked") 140 | Function fun = 141 | ConstantMemoizer.memoizer( 142 | (Function) (Function) x -> x, String.class, Integer.class); 143 | assertThrows(ClassCastException.class, () -> fun.apply("boom !")); 144 | } 145 | 146 | @Test 147 | public void testObjectWrongParameterType() { 148 | @SuppressWarnings("unchecked") 149 | Function fun = 150 | ConstantMemoizer.memoizer( 151 | (Function) (Function) (String x) -> x, 152 | Integer.class, 153 | String.class); 154 | assertThrows(ClassCastException.class, () -> fun.apply(666)); 155 | } 156 | 157 | @Test 158 | public void testIntWrongParameterType() { 159 | @SuppressWarnings("unchecked") 160 | ToIntFunction fun = 161 | ConstantMemoizer.intMemoizer( 162 | (ToIntFunction) (ToIntFunction) (String x) -> Integer.parseInt(x), 163 | Integer.class); 164 | assertThrows(ClassCastException.class, () -> fun.applyAsInt(666)); 165 | } 166 | 167 | @Test 168 | public void testLongWrongParameterType() { 169 | @SuppressWarnings("unchecked") 170 | ToLongFunction fun = 171 | ConstantMemoizer.longMemoizer( 172 | (ToLongFunction) (ToLongFunction) (String x) -> Long.parseLong(x), 173 | Integer.class); 174 | assertThrows(ClassCastException.class, () -> fun.applyAsLong(666)); 175 | } 176 | 177 | @Test 178 | public void testDoubleWrongParameterType() { 179 | @SuppressWarnings("unchecked") 180 | ToDoubleFunction fun = 181 | ConstantMemoizer.doubleMemoizer( 182 | (ToDoubleFunction) (ToDoubleFunction) (String x) -> Double.parseDouble(x), 183 | Integer.class); 184 | assertThrows(ClassCastException.class, () -> fun.applyAsDouble(666)); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/MostlyConstant.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.constant; 4 | 5 | import java.lang.invoke.MethodHandle; 6 | import java.lang.invoke.MutableCallSite; 7 | import java.util.Objects; 8 | import java.util.function.DoubleSupplier; 9 | import java.util.function.IntSupplier; 10 | import java.util.function.LongSupplier; 11 | import java.util.function.Supplier; 12 | 13 | /** 14 | * Create a constant value that can be changed from time to time. If the {@link #getter()} is stored 15 | * in a static final field, the result of the supplier is guaranteed to be seen as a constant by the 16 | * Virtual Machine. 17 | * 18 | *

To avoid unnecessary boxing in common cases of constant of type {@code int}, {@code long} and 19 | * {@code double}, there are specialized version of the {@link #getter()}, {@link #intGetter()}, 20 | * {@link #longGetter()} and {@link #doubleGetter()}. 21 | * 22 | *

This work because when {@link #setAndDeoptimize(Object)} is called, all the assembly code 23 | * (JITed code) that where containing the constant are de-optimized and in the future they will be 24 | * re-optimized with the new value of the constant. So calling {@link #setAndDeoptimize(Object)} in 25 | * a loop will kill performance. 26 | * 27 | *

Example of usage 28 | * 29 | *

 30 |  *   private static final MostlyConstant<Integer> FOO = new MostlyConstant<>(42, int.class);
 31 |  *   private static final IntSupplier FOO_GETTER = FOO.intGetter();
 32 |  *
 33 |  *   public static int getFoo() {
 34 |  *     return FOO_GETTER.getAsInt();
 35 |  *   }
 36 |  *   public static void setFoo(int value) {
 37 |  *     FOO.setAndDeoptimize(value);
 38 |  *   }
 39 |  * 
40 | * 41 | * @param the type of the constant. 42 | */ 43 | public final class MostlyConstant { 44 | private final Class type; 45 | private final MutableCallSite callSite; 46 | private final MethodHandle invoker; 47 | 48 | /** 49 | * Create a constant with a value ({@code constant}) and its class ({@code type}). 50 | * 51 | * @param constant the value of the constant. 52 | * @param type the class of the constant. 53 | * @throws NullPointerException if type is null. 54 | * @throws ClassCastException if the constant cannot be converted to the type 55 | * @throws IllegalArgumentException is type is void.class 56 | */ 57 | public MostlyConstant(T constant, Class type) { 58 | this.type = Objects.requireNonNull(type); 59 | MethodHandle target = constant(type, constant); 60 | MutableCallSite callSite = new MutableCallSite(target.asType(target.type().erase())); 61 | this.callSite = callSite; 62 | this.invoker = callSite.dynamicInvoker(); 63 | } 64 | 65 | /** 66 | * Change the value of the constant. This call requires the VM to de-optimize all the assembly 67 | * codes that contains the previous value of this constant, so this call will slow down the 68 | * application. Use this method with care, you have been warned. 69 | * 70 | * @param constant the new value of the constant. 71 | * @throws ClassCastException if the constant cannot be converted to constant type. 72 | */ 73 | public void setAndDeoptimize(T constant) { 74 | MethodHandle target = constant(type, constant); 75 | callSite.setTarget(target.asType(callSite.type())); 76 | MutableCallSite.syncAll(new MutableCallSite[] { callSite }); 77 | } 78 | 79 | /** 80 | * Returns a supplier that will return the value of this constant as a constant value. The 81 | * returned supplier should be stored in a static field for performance. 82 | * 83 | * @return a supplier that will return the value of this constant as a constant value. 84 | * @see MostlyConstant#intGetter() 85 | * @see MostlyConstant#longGetter() 86 | * @see MostlyConstant#doubleGetter() 87 | */ 88 | public Supplier getter() { 89 | MethodHandle invoker = this.invoker; 90 | return () -> { 91 | try { 92 | return (T) invoker.invokeExact(); 93 | } catch (Throwable e) { 94 | throw Thrower.rethrow(e); 95 | } 96 | }; 97 | /*return new Supplier<>() { 98 | @Override 99 | public T get() { 100 | try { 101 | return (T)invoker.invokeExact(); 102 | } catch (Throwable e) { 103 | throw Thrower.rethrow(e); 104 | } 105 | } 106 | };*/ 107 | } 108 | 109 | /** 110 | * Returns a supplier that will return the value of this constant as a constant value. The 111 | * returned supplier should be stored in a static field for performance. 112 | * 113 | * @return a supplier that will return the value of this constant as a constant value. 114 | * @throws IllegalStateException if the constant is not of type {@code int.class}. 115 | * @see MostlyConstant#getter() 116 | */ 117 | public IntSupplier intGetter() { 118 | if (callSite.type().returnType() != int.class) { 119 | throw new IllegalStateException("the constant is not of type int.class"); 120 | } 121 | MethodHandle invoker = this.invoker; 122 | return () -> { 123 | try { 124 | return (int) invoker.invokeExact(); 125 | } catch (Throwable e) { 126 | throw Thrower.rethrow(e); 127 | } 128 | }; 129 | } 130 | 131 | /** 132 | * Returns a supplier that will return the value of this constant as a constant value. The 133 | * returned supplier should be stored in a static field for performance. 134 | * 135 | * @return a supplier that will return the value of this constant as a constant value. 136 | * @throws IllegalStateException if the constant is not of type {@code long.class}. 137 | * @see MostlyConstant#getter() 138 | */ 139 | public LongSupplier longGetter() { 140 | if (callSite.type().returnType() != long.class) { 141 | throw new IllegalStateException("the constant is not of type long.class"); 142 | } 143 | MethodHandle invoker = this.invoker; 144 | return () -> { 145 | try { 146 | return (long) invoker.invokeExact(); 147 | } catch (Throwable e) { 148 | throw Thrower.rethrow(e); 149 | } 150 | }; 151 | } 152 | 153 | /** 154 | * Returns a supplier that will return the value of this constant as a constant value. The 155 | * returned supplier should be stored in a static field for performance. 156 | * 157 | * @return a supplier that will return the value of this constant as a constant value. 158 | * @throws IllegalStateException if the constant is not of type {@code double.class}. 159 | * @see MostlyConstant#getter() 160 | */ 161 | public DoubleSupplier doubleGetter() { 162 | if (callSite.type().returnType() != double.class) { 163 | throw new IllegalStateException("the constant is not of type double.class"); 164 | } 165 | MethodHandle invoker = this.invoker; 166 | return () -> { 167 | try { 168 | return (double) invoker.invokeExact(); 169 | } catch (Throwable e) { 170 | throw Thrower.rethrow(e); 171 | } 172 | }; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/perf/VisitorBenchMark.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic.perf; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.function.BiFunction; 7 | 8 | import org.openjdk.jmh.annotations.Benchmark; 9 | import org.openjdk.jmh.annotations.BenchmarkMode; 10 | import org.openjdk.jmh.annotations.Fork; 11 | import org.openjdk.jmh.annotations.Measurement; 12 | import org.openjdk.jmh.annotations.Mode; 13 | import org.openjdk.jmh.annotations.OutputTimeUnit; 14 | import org.openjdk.jmh.annotations.Scope; 15 | import org.openjdk.jmh.annotations.State; 16 | import org.openjdk.jmh.annotations.Warmup; 17 | import org.openjdk.jmh.runner.Runner; 18 | import org.openjdk.jmh.runner.RunnerException; 19 | import org.openjdk.jmh.runner.options.Options; 20 | import org.openjdk.jmh.runner.options.OptionsBuilder; 21 | 22 | import com.github.forax.exotic.Visitor; 23 | 24 | @Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) 25 | @Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) 26 | @Fork(3) 27 | @BenchmarkMode(Mode.AverageTime) 28 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 29 | @State(Scope.Benchmark) 30 | public class VisitorBenchMark { 31 | 32 | interface Expr { 33 | R accept(GofVisitor visitor, P parameter); 34 | } 35 | static class Value implements Expr { 36 | final int value; Value(int value) { this.value = value; } 37 | @Override 38 | public R accept(GofVisitor visitor, P parameter) { 39 | return visitor.visitValue(this, parameter); 40 | } 41 | } 42 | static class Add implements Expr { 43 | final Expr left, right; Add(Expr left, Expr right) { this.left = left; this.right = right; } 44 | @Override 45 | public R accept(GofVisitor visitor, P parameter) { 46 | return visitor.visitAdd(this, parameter); 47 | } 48 | } 49 | static class Var implements Expr { 50 | final String name; Var(String name) { this.name = name; } 51 | @Override 52 | public R accept(GofVisitor visitor, P parameter) { 53 | return visitor.visitVar(this, parameter); 54 | } 55 | } 56 | static class Assign implements Expr { 57 | final String name; final Expr expr; Assign(String name, Expr expr) { this.name = name; this.expr = expr; } 58 | @Override 59 | public R accept(GofVisitor visitor, P parameter) { 60 | return visitor.visitAssign(this, parameter); 61 | } 62 | } 63 | static class Block implements Expr { 64 | final List exprs; Block(List exprs) { this.exprs = exprs; } 65 | @Override 66 | public R accept(GofVisitor visitor, P parameter) { 67 | return visitor.visitBlock(this, parameter); 68 | } 69 | } 70 | 71 | interface GofVisitor { 72 | R visitValue(Value value, P parameter); 73 | R visitAdd(Add add, P parameter); 74 | R visitVar(Var var, P parameter); 75 | R visitAssign(Assign assign, P parameter); 76 | R visitBlock(Block block, P parameter); 77 | } 78 | 79 | static class MapVisitor { 80 | private final HashMap, BiFunction> map = new HashMap<>(); 81 | 82 | @SuppressWarnings("unchecked") 83 | public MapVisitor register(Class type, BiFunction fun) { 84 | map.put(type, (BiFunction)fun); 85 | return this; 86 | } 87 | 88 | public R visit(Object expr, P parameter) { 89 | return map.getOrDefault(expr.getClass(), (_1, _2) -> { throw new IllegalStateException(); }).apply(expr, parameter); 90 | } 91 | } 92 | 93 | class Env { 94 | final HashMap vars = new HashMap<>(); 95 | } 96 | 97 | private static final Expr CODE = new Block(List.of( 98 | new Assign("c", new Block(List.of( 99 | new Assign("a", new Value(20)), 100 | new Var("e"), 101 | new Assign("b", new Add(new Value(5), new Var("c"))), 102 | new Assign("d", new Var("b")), 103 | new Add(new Add(new Var("a"), new Value(11)), new Var("d")) 104 | )) 105 | ), 106 | new Add(new Value(2), new Var("b")) 107 | )); 108 | 109 | private static final Visitor EXOTIC_VISITOR = Visitor.create(Env.class, int.class, opt -> opt 110 | .register(Value.class, (v, value, env) -> value.value) 111 | .register(Add.class, (v, add, env) -> v.visit(add.left, env) + v.visit(add.right, env)) 112 | .register(Var.class, (v, var, env) -> env.vars.getOrDefault(var.name, 0)) 113 | .register(Assign.class, (v, assign, env) -> { int let = v.visit(assign.expr, env); env.vars.put(assign.name, let); return let; }) 114 | .register(Block.class, (v, block, env) -> { int result = 0; for(Expr expr: block.exprs) { result = v.visit(expr, env); } return result; }) 115 | ); 116 | 117 | private static final GofVisitor GOF_VISITOR = new GofVisitor<>() { 118 | @Override 119 | public Integer visitValue(Value value, Env env) { return value.value; } 120 | @Override 121 | public Integer visitAdd(Add add, Env env) { return add.left.accept(this, env) + add.right.accept(this, env); } 122 | @Override 123 | public Integer visitVar(Var var, Env env) { return env.vars.getOrDefault(var.name, 0); } 124 | @Override 125 | public Integer visitAssign(Assign assign, Env env) { int let = assign.expr.accept(this, env); env.vars.put(assign.name, let); return let; } 126 | @Override 127 | public Integer visitBlock(Block block, Env env) { int result = 0; for(Expr expr: block.exprs) { result = expr.accept(this, env); } return result; } 128 | }; 129 | 130 | private static final MapVisitor MAP_VISITOR = new MapVisitor<>(); 131 | static { 132 | MAP_VISITOR.register(Value.class, (value, env) -> value.value) 133 | .register(Add.class, (add, env) -> MAP_VISITOR.visit(add.left, env) + MAP_VISITOR.visit(add.right, env)) 134 | .register(Var.class, (var, env) -> env.vars.getOrDefault(var.name, 0)) 135 | .register(Assign.class, (assign, env) -> { int let = MAP_VISITOR.visit(assign.expr, env); env.vars.put(assign.name, let); return let; }) 136 | .register(Block.class, (block, env) -> { int result = 0; for(Expr expr: block.exprs) { result = MAP_VISITOR.visit(expr, env); } return result; }); 137 | } 138 | 139 | 140 | @Benchmark 141 | public int map_visitor() { 142 | return MAP_VISITOR.visit(CODE, new Env()); 143 | } 144 | 145 | @Benchmark 146 | public int exotic_visitor() { 147 | return EXOTIC_VISITOR.visit(CODE, new Env()); 148 | } 149 | 150 | @Benchmark 151 | public int gof_visitor() { 152 | return CODE.accept(GOF_VISITOR, new Env()); 153 | } 154 | 155 | 156 | public static void main(String[] args) throws RunnerException { 157 | Options opt = new OptionsBuilder().include(VisitorBenchMark.class.getName()).build(); 158 | new Runner(opt).run(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/perf/StringSwitchBenchMark.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic.perf; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.util.stream.Stream; 5 | 6 | import org.openjdk.jmh.annotations.Benchmark; 7 | import org.openjdk.jmh.annotations.BenchmarkMode; 8 | import org.openjdk.jmh.annotations.Fork; 9 | import org.openjdk.jmh.annotations.Measurement; 10 | import org.openjdk.jmh.annotations.Mode; 11 | import org.openjdk.jmh.annotations.OutputTimeUnit; 12 | import org.openjdk.jmh.annotations.Scope; 13 | import org.openjdk.jmh.annotations.State; 14 | import org.openjdk.jmh.annotations.Warmup; 15 | import org.openjdk.jmh.runner.Runner; 16 | import org.openjdk.jmh.runner.RunnerException; 17 | import org.openjdk.jmh.runner.options.Options; 18 | import org.openjdk.jmh.runner.options.OptionsBuilder; 19 | 20 | import com.github.forax.exotic.StringSwitch; 21 | 22 | @SuppressWarnings("static-method") 23 | @Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) 24 | @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) 25 | @Fork(3) 26 | @BenchmarkMode(Mode.AverageTime) 27 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 28 | @State(Scope.Benchmark) 29 | public class StringSwitchBenchMark { 30 | private static final StringSwitch SMALL_STRING_SWITCH = StringSwitch.create(false, 31 | "elephant", "girafe", "springbok", "monkey"/*, "snake", "crocodile", "orangoutang", "opossum", "tiger", "hippopotamus", "koala" */); 32 | 33 | private static final StringSwitch BIG_STRING_SWITCH = StringSwitch.create(false, 34 | "elephant", "girafe", "springbok", "monkey", "snake", "crocodile", "orangoutang", "opossum", "tiger", "hippopotamus", "koala"); 35 | 36 | private static final String[] DATA = Stream.of( 37 | "lion", "elephant", "springbok", "elephant", "girafe", "snake", "crocodile", "elephant", "monkey", 38 | "girafe", "hippopotamus", "opossum", "elephant", "girafe", "snake", "opossum", "lion", "tiger", "snake", "koala") 39 | .map(String::new) 40 | .toArray(String[]::new); 41 | 42 | @Benchmark 43 | public int small_small_string_switch() { 44 | int sum = 0; 45 | for(int i = 0; i < 4; i++) { 46 | String s = DATA[i]; 47 | sum += SMALL_STRING_SWITCH.stringSwitch(s); 48 | } 49 | return sum; 50 | } 51 | 52 | @Benchmark 53 | public int small_small_ifequals_cascade() { 54 | int sum = 0; 55 | for(int i = 0; i < 4; i++) { 56 | String s = DATA[i]; 57 | int value; 58 | if (s.equals("elephant")) { value = 0; } 59 | else if (s.equals("girafe")) { value = 1; } 60 | else if (s.equals("springbok")) { value = 2; } 61 | else if (s.equals("monkey")) { value = 3; } 62 | else { value = StringSwitch.NO_MATCH; } 63 | sum += value; 64 | } 65 | return sum; 66 | } 67 | 68 | @Benchmark 69 | public int small_small_oldswitch() { 70 | int sum = 0; 71 | for(int i = 0; i < 4; i++) { 72 | String s = DATA[i]; 73 | int value; 74 | switch(s) { 75 | case "elephant": 76 | value = 0; 77 | break; 78 | case "girafe": 79 | value = 1; 80 | break; 81 | case "springbok": 82 | value = 2; 83 | break; 84 | case "monkey": 85 | value = 3; 86 | break; 87 | default: 88 | value = StringSwitch.NO_MATCH; 89 | } 90 | sum += value; 91 | } 92 | return sum; 93 | } 94 | 95 | /*@Benchmark 96 | public int small_big_string_switch() { 97 | int sum = 0; 98 | for(String s: DATA) { 99 | sum += SMALL_STRING_SWITCH.stringSwitch(s); 100 | } 101 | return sum; 102 | } 103 | 104 | @Benchmark 105 | public int small_big_ifequals_cascade() { 106 | int sum = 0; 107 | for(int i = 0; i < 4; i++) { 108 | String s = DATA[i]; 109 | int value; 110 | if (s.equals("elephant")) { value = 0; } 111 | else if (s.equals("girafe")) { value = 1; } 112 | else if (s.equals("springbok")) { value = 2; } 113 | else if (s.equals("monkey")) { value = 3; } 114 | else { value = StringSwitch.NO_MATCH; } 115 | sum += value; 116 | } 117 | return sum; 118 | } 119 | 120 | @Benchmark 121 | public int small_big_oldswitch() { 122 | int sum = 0; 123 | for(String s: DATA) { 124 | int value; 125 | switch(s) { 126 | case "elephant": 127 | value = 0; 128 | break; 129 | case "girafe": 130 | value = 1; 131 | break; 132 | case "springbok": 133 | value = 2; 134 | break; 135 | case "monkey": 136 | value = 3; 137 | break; 138 | default: 139 | value = StringSwitch.NO_MATCH; 140 | } 141 | sum += value; 142 | } 143 | return sum; 144 | }*/ 145 | 146 | @Benchmark 147 | public int big_big_string_switch() { 148 | int sum = 0; 149 | for(String s: DATA) { 150 | sum += BIG_STRING_SWITCH.stringSwitch(s); 151 | } 152 | return sum; 153 | } 154 | 155 | @Benchmark 156 | public int big_big_ifequals_cascade() { 157 | int sum = 0; 158 | for(int i = 0; i < 4; i++) { 159 | String s = DATA[i]; 160 | int value; 161 | if (s.equals("elephant")) { value = 0; } 162 | else if (s.equals("girafe")) { value = 1; } 163 | else if (s.equals("springbok")) { value = 2; } 164 | else if (s.equals("monkey")) { value = 3; } 165 | else if (s.equals("snake")) { value = 4; } 166 | else if (s.equals("crocodile")) { value = 5; } 167 | else if (s.equals("orangoutang")) { value = 6; } 168 | else if (s.equals("opossum")) { value = 7; } 169 | else if (s.equals("tiger")) { value = 8; } 170 | else if (s.equals("hippopotamus")) { value = 9; } 171 | else if (s.equals("koala")) { value = 10; } 172 | else { value = StringSwitch.NO_MATCH; } 173 | sum += value; 174 | } 175 | return sum; 176 | } 177 | 178 | @Benchmark 179 | public int big_big_oldswitch() { 180 | int sum = 0; 181 | for(String s: DATA) { 182 | int value; 183 | switch(s) { 184 | case "elephant": 185 | value = 0; 186 | break; 187 | case "girafe": 188 | value = 1; 189 | break; 190 | case "springbok": 191 | value = 2; 192 | break; 193 | case "monkey": 194 | value = 3; 195 | break; 196 | case "snake": 197 | value = 4; 198 | break; 199 | case "crocodile": 200 | value = 5; 201 | break; 202 | case "orangoutang": 203 | value = 6; 204 | break; 205 | case "opossum": 206 | value = 7; 207 | break; 208 | case "tiger": 209 | value = 8; 210 | break; 211 | case "hippopotamus": 212 | value = 9; 213 | break; 214 | case "koala": 215 | value = 10; 216 | break; 217 | default: 218 | value = StringSwitch.NO_MATCH; 219 | } 220 | sum += value; 221 | } 222 | return sum; 223 | } 224 | 225 | public static void main(String[] args) throws RunnerException { 226 | Options opt = new OptionsBuilder().include(StringSwitchBenchMark.class.getName()).build(); 227 | new Runner(opt).run(); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/StructuralCallImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.dropArguments; 4 | import static java.lang.invoke.MethodHandles.guardWithTest; 5 | import static java.lang.invoke.MethodHandles.insertArguments; 6 | import static java.lang.invoke.MethodType.genericMethodType; 7 | import static java.lang.invoke.MethodType.methodType; 8 | import static java.util.Collections.nCopies; 9 | 10 | import java.lang.invoke.MethodHandle; 11 | import java.lang.invoke.MethodHandles; 12 | import java.lang.invoke.MethodHandles.Lookup; 13 | import java.lang.invoke.MethodType; 14 | import java.lang.invoke.MutableCallSite; 15 | import java.util.ArrayDeque; 16 | import java.util.Objects; 17 | 18 | interface StructuralCallImpl extends StructuralCall { 19 | @Override 20 | @SuppressWarnings("unchecked") 21 | default R invoke(Object receiver) { 22 | return (R) call(1, receiver, null, null, null, null, null, null, null, null); 23 | } 24 | 25 | @Override 26 | @SuppressWarnings("unchecked") 27 | default R invoke(Object receiver, Object arg1) { 28 | return (R) call(2, receiver, arg1, null, null, null, null, null, null, null); 29 | } 30 | 31 | @Override 32 | @SuppressWarnings("unchecked") 33 | default R invoke(Object receiver, Object arg1, Object arg2) { 34 | return (R) call(3, receiver, arg1, arg2, null, null, null, null, null, null); 35 | } 36 | 37 | @Override 38 | @SuppressWarnings("unchecked") 39 | default R invoke(Object receiver, Object arg1, Object arg2, Object arg3) { 40 | return (R) call(4, receiver, arg1, arg2, arg3, null, null, null, null, null); 41 | } 42 | 43 | @Override 44 | @SuppressWarnings("unchecked") 45 | default R invoke(Object receiver, Object arg1, Object arg2, Object arg3, Object arg4) { 46 | return (R) call(5, receiver, arg1, arg2, arg3, arg4, null, null, null, null); 47 | } 48 | 49 | @Override 50 | @SuppressWarnings("unchecked") 51 | default R invoke( 52 | Object receiver, Object arg1, Object arg2, Object arg3, Object arg4, Object arg5) { 53 | return (R) call(6, receiver, arg1, arg2, arg3, arg4, arg5, null, null, null); 54 | } 55 | 56 | @Override 57 | @SuppressWarnings("unchecked") 58 | default R invoke( 59 | Object receiver, 60 | Object arg1, 61 | Object arg2, 62 | Object arg3, 63 | Object arg4, 64 | Object arg5, 65 | Object arg6) { 66 | return (R) call(7, receiver, arg1, arg2, arg3, arg4, arg5, arg6, null, null); 67 | } 68 | 69 | @Override 70 | @SuppressWarnings("unchecked") 71 | default R invoke( 72 | Object receiver, 73 | Object arg1, 74 | Object arg2, 75 | Object arg3, 76 | Object arg4, 77 | Object arg5, 78 | Object arg6, 79 | Object arg7) { 80 | return (R) call(8, receiver, arg1, arg2, arg3, arg4, arg5, arg6, arg7, null); 81 | } 82 | 83 | @Override 84 | @SuppressWarnings("unchecked") 85 | default R invoke( 86 | Object receiver, 87 | Object arg1, 88 | Object arg2, 89 | Object arg3, 90 | Object arg4, 91 | Object arg5, 92 | Object arg6, 93 | Object arg7, 94 | Object arg8) { 95 | return (R) call(9, receiver, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); 96 | } 97 | 98 | Object call( 99 | int argCount, 100 | Object receiver, 101 | Object arg1, 102 | Object arg2, 103 | Object arg3, 104 | Object arg4, 105 | Object arg5, 106 | Object arg6, 107 | Object arg7, 108 | Object arg8); 109 | 110 | static MethodHandle findMethodHandle(Lookup lookup, String name, MethodType type) { 111 | Objects.requireNonNull(lookup); 112 | Objects.requireNonNull(name); 113 | Objects.requireNonNull(type); 114 | MethodHandle mh = 115 | new InliningCacheCallSite(type.insertParameterTypes(0, Object.class), lookup, name) 116 | .dynamicInvoker(); 117 | int parameterCount = mh.type().parameterCount(); 118 | if (parameterCount != 9) { 119 | mh = dropArguments(mh, parameterCount, nCopies(9 - parameterCount, Object.class)); 120 | } 121 | mh = mh.asType(genericMethodType(9)); 122 | 123 | // check that parameterCount == argCount 124 | MethodHandle guard = 125 | guardWithTest( 126 | insertArguments(InliningCacheCallSite.COUNTCHECK, 0, parameterCount), 127 | dropArguments(mh, 0, int.class), 128 | dropArguments(InliningCacheCallSite.ERRORCOUNT, 1, mh.type().parameterList())); 129 | return guard; 130 | } 131 | 132 | class InliningCacheCallSite extends MutableCallSite { 133 | private static final MethodHandle FALLBACK, TYPECHECK; 134 | static final MethodHandle COUNTCHECK, ERRORCOUNT; 135 | 136 | static { 137 | Lookup lookup = MethodHandles.lookup(); 138 | try { 139 | FALLBACK = 140 | lookup.findVirtual( 141 | InliningCacheCallSite.class, 142 | "fallback", 143 | methodType(MethodHandle.class, Object.class)); 144 | TYPECHECK = 145 | lookup.findStatic( 146 | InliningCacheCallSite.class, 147 | "typecheck", 148 | methodType(boolean.class, Class.class, Object.class)); 149 | COUNTCHECK = 150 | lookup.findStatic( 151 | InliningCacheCallSite.class, 152 | "countcheck", 153 | methodType(boolean.class, int.class, int.class)); 154 | ERRORCOUNT = 155 | lookup.findStatic( 156 | InliningCacheCallSite.class, "errorcount", methodType(Object.class, int.class)); 157 | } catch (NoSuchMethodException | IllegalAccessException e) { 158 | throw new AssertionError(e); 159 | } 160 | } 161 | 162 | private final Lookup lookup; 163 | private final String name; 164 | 165 | InliningCacheCallSite(MethodType type, Lookup lookup, String name) { 166 | super(type); 167 | this.lookup = lookup; 168 | this.name = name; 169 | setTarget( 170 | MethodHandles.foldArguments(MethodHandles.exactInvoker(type), FALLBACK.bindTo(this))); 171 | } 172 | 173 | @SuppressWarnings("unused") 174 | private MethodHandle fallback(Object receiver) { 175 | Class receiverClass = receiver.getClass(); 176 | MethodHandle target; 177 | try { 178 | target = findTarget(lookup, receiverClass, name, type().dropParameterTypes(0, 1)); 179 | } catch (NoSuchMethodException e) { 180 | throw (NoSuchMethodError) new NoSuchMethodError().initCause(e); 181 | } catch (IllegalAccessException e) { 182 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 183 | } 184 | 185 | target = target.asType(type()); 186 | MethodHandle guard = 187 | MethodHandles.guardWithTest( 188 | TYPECHECK.bindTo(receiverClass), 189 | target, 190 | new InliningCacheCallSite(type(), lookup, name).dynamicInvoker()); 191 | setTarget(guard); 192 | return target; 193 | } 194 | 195 | private static MethodHandle findTarget( 196 | Lookup lookup, Class receiverClass, String name, MethodType parameterType) 197 | throws IllegalAccessException, NoSuchMethodException { 198 | ArrayDeque> queue = new ArrayDeque<>(); 199 | queue.add(receiverClass); 200 | 201 | Class type; 202 | IllegalAccessException illegalAccess = null; 203 | while ((type = queue.poll()) != null) { 204 | try { 205 | return lookup.findVirtual(type, name, parameterType); 206 | } catch (NoSuchMethodException e) { 207 | if (illegalAccess == null) { 208 | throw e; 209 | } 210 | } catch (IllegalAccessException e) { 211 | if (illegalAccess == null) { 212 | illegalAccess = e; 213 | } 214 | 215 | // try super types 216 | queue.add(type.getSuperclass()); 217 | for (Class interfaze : type.getInterfaces()) { 218 | queue.add(interfaze); 219 | } 220 | } 221 | } 222 | assert illegalAccess != null; 223 | throw illegalAccess; 224 | } 225 | 226 | @SuppressWarnings("unused") 227 | private static boolean typecheck(Class type, Object o) { 228 | return o.getClass() == type; 229 | } 230 | 231 | @SuppressWarnings("unused") 232 | private static boolean countcheck(int parameterCount, int argumentCount) { 233 | return parameterCount == argumentCount; 234 | } 235 | 236 | @SuppressWarnings("unused") 237 | private static Object errorcount(int argumentCount) { 238 | throw new IllegalArgumentException("wrong number of argument " + argumentCount); 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/TypeSwitchCallSite.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.constant; 4 | import static java.lang.invoke.MethodHandles.dropArguments; 5 | import static java.lang.invoke.MethodHandles.guardWithTest; 6 | import static java.lang.invoke.MethodType.methodType; 7 | 8 | import java.lang.invoke.MethodHandle; 9 | import java.lang.invoke.MethodHandles; 10 | import java.lang.invoke.MethodHandles.Lookup; 11 | import java.lang.invoke.MethodType; 12 | import java.lang.invoke.MutableCallSite; 13 | import java.lang.ref.WeakReference; 14 | import java.util.HashMap; 15 | import java.util.Objects; 16 | 17 | class TypeSwitchCallSite extends MutableCallSite { 18 | static void validatePartialOrder(Class[] typecases) { 19 | int length = typecases.length; 20 | if (length == 0 || length == 1) { 21 | return; 22 | } 23 | HashMap, Class> map = new HashMap<>(); //FIXME pre-size ?? 24 | for (int i = length; --i >= 0;) { 25 | Class typecase = typecases[i]; 26 | Objects.requireNonNull(typecase); 27 | validateType(map, typecase); 28 | } 29 | } 30 | 31 | private static void validateType(HashMap, Class> map, Class typecase) { 32 | Class conflictingCaseType = map.putIfAbsent(typecase, typecase); 33 | if (conflictingCaseType != null) { 34 | throw new IllegalStateException( 35 | "Case " + conflictingCaseType.getName() + " matches a subtype of what case " + 36 | typecase.getName() + " matches but is located after it"); 37 | } 38 | validateSupertypes(map, typecase, typecase); 39 | } 40 | 41 | private static void validateSupertypes(HashMap, Class> map, Class type, Class typecase) { 42 | Class superclass = type.getSuperclass(); 43 | if (superclass == null && type != Object.class) { 44 | superclass = Object.class; // interfaces are subtypes of Object 45 | } 46 | if (superclass != null && map.putIfAbsent(superclass, typecase) == null) { 47 | validateSupertypes(map, superclass, typecase); 48 | } 49 | for (Class superinterface : type.getInterfaces()) { 50 | if (map.putIfAbsent(superinterface, typecase) == null) { 51 | validateSupertypes(map, superinterface, typecase); 52 | } 53 | } 54 | } 55 | 56 | private interface Strategy { 57 | int index(Class receiverClass); 58 | MethodHandle target(); 59 | 60 | static Strategy isInstance(Class[] typecases) { 61 | WeakReference>[] refs = createRefArray(typecases); 62 | return new Strategy() { 63 | @Override 64 | public int index(Class receiverClass) { 65 | for(int i = 0; i < refs.length; i++) { 66 | Class typecase = refs[i].get(); 67 | if (typecase != null && typecase.isAssignableFrom(receiverClass)) { 68 | return i; 69 | } 70 | } 71 | return TypeSwitch.NO_MATCH; 72 | } 73 | @Override 74 | public MethodHandle target() { 75 | MethodHandle mh = dropArguments(constant(int.class, TypeSwitch.NO_MATCH), 0, Object.class); 76 | for(int i = refs.length; --i >= 0;) { 77 | Class typecase = refs[i].get(); 78 | if (typecase == null) { 79 | continue; 80 | } 81 | mh = guardWithTest(IS_INSTANCE.bindTo(typecase), 82 | dropArguments(constant(int.class, i), 0, Object.class), 83 | mh); 84 | } 85 | return mh; 86 | } 87 | }; 88 | } 89 | 90 | static Strategy classValue(Class[] typecases) { 91 | ClassValue classValue = createClassValue(typecases); 92 | return new Strategy() { 93 | @Override 94 | public int index(Class receiverClass) { 95 | return classValue.get(receiverClass); 96 | } 97 | @Override 98 | public MethodHandle target(/*Lookup lookup*/) { 99 | return GET.bindTo(classValue); 100 | } 101 | }; 102 | } 103 | } 104 | 105 | static WeakReference>[] createRefArray(Class[] typecases) { 106 | @SuppressWarnings("unchecked") 107 | WeakReference>[] refs = (WeakReference>[])new WeakReference[typecases.length]; 108 | for(int i = 0; i < typecases.length; i++) { 109 | refs[i] = new WeakReference<>(typecases[i]); 110 | } 111 | return refs; 112 | } 113 | 114 | static ClassValue createClassValue(Class[] typecases) { 115 | ThreadLocal local = new ThreadLocal<>(); 116 | ClassValue classValue = new ClassValue() { 117 | @Override 118 | protected Integer computeValue(Class type) { 119 | Integer index = local.get(); 120 | if (index != null) { // injection 121 | return index; 122 | } 123 | return computeFromSupertypes(type); 124 | } 125 | 126 | private Integer computeFromSupertypes(Class type) { 127 | int index = TypeSwitch.NO_MATCH; 128 | Class superclass = type.getSuperclass(); 129 | if (superclass != null) { 130 | index = get(superclass); 131 | } 132 | for(Class supertype: type.getInterfaces()) { 133 | int localIndex = get(supertype); 134 | if (localIndex != TypeSwitch.NO_MATCH) { 135 | index = (index == TypeSwitch.NO_MATCH)? localIndex: Math.min(index, localIndex); 136 | } 137 | } 138 | return index; 139 | } 140 | }; 141 | for(int i = 0; i < typecases.length; i++) { 142 | local.set(i); // inject value 143 | classValue.get(typecases[i]); 144 | } 145 | local.set(null); // no injection anymore 146 | return classValue; 147 | } 148 | 149 | 150 | private static final MethodType OBJECT_TO_INT = methodType(int.class, Object.class); 151 | private static final MethodHandle FALLBACK, TYPECHECK, NULLCHECK; 152 | static final MethodHandle GET, IS_INSTANCE; 153 | static { 154 | Lookup lookup = MethodHandles.lookup(); 155 | try { 156 | FALLBACK = lookup.findVirtual(TypeSwitchCallSite.class, "fallback", OBJECT_TO_INT); 157 | TYPECHECK = lookup.findStatic(TypeSwitchCallSite.class, "typecheck", methodType(boolean.class, Class.class, Object.class)); 158 | GET = lookup.findStatic(TypeSwitchCallSite.class, "get", methodType(int.class, ClassValue.class, Object.class)); 159 | NULLCHECK = lookup.findStatic(Objects.class, "isNull", methodType(boolean.class, Object.class)); 160 | IS_INSTANCE = lookup.findVirtual(Class.class, "isInstance", methodType(boolean.class, Object.class)); 161 | } catch(NoSuchMethodException | IllegalAccessException e) { 162 | throw new AssertionError(e); 163 | } 164 | } 165 | 166 | private static final int MAX_DEPTH = 8; 167 | private static final int STRATEGY_CUT_OFF = 5; 168 | 169 | private final int depth; 170 | private final TypeSwitchCallSite callsite; 171 | private final Strategy strategy; 172 | 173 | private TypeSwitchCallSite(Strategy strategy) { 174 | super(OBJECT_TO_INT); 175 | this.depth = 0; 176 | this.callsite = this; 177 | this.strategy = strategy; 178 | setTarget(FALLBACK.bindTo(this)); 179 | } 180 | 181 | private TypeSwitchCallSite(int depth, TypeSwitchCallSite callsite, Strategy strategy) { 182 | super(OBJECT_TO_INT); 183 | this.depth = depth; 184 | this.callsite = callsite; 185 | this.strategy = strategy; 186 | setTarget(FALLBACK.bindTo(this)); 187 | } 188 | 189 | static TypeSwitchCallSite create(Class[] typecases) { 190 | for(Class typecase: typecases) { 191 | Objects.requireNonNull(typecase); 192 | } 193 | 194 | Strategy strategy = (typecases.length < STRATEGY_CUT_OFF)? 195 | Strategy.isInstance(typecases): Strategy.classValue(typecases); 196 | return new TypeSwitchCallSite(strategy); 197 | } 198 | 199 | @SuppressWarnings("unused") 200 | private int fallback(Object value) { 201 | Class receiverClass = value.getClass(); 202 | int index = strategy.index(receiverClass); 203 | 204 | if (depth == MAX_DEPTH) { 205 | setTarget(strategy.target()); 206 | return index; 207 | } 208 | 209 | setTarget(guardWithTest(TYPECHECK.bindTo(receiverClass), 210 | dropArguments(constant(int.class, index), 0, Object.class), 211 | new TypeSwitchCallSite(depth + 1, callsite, strategy).dynamicInvoker())); 212 | return index; 213 | } 214 | 215 | @SuppressWarnings("unused") 216 | private static boolean typecheck(Class type, Object value) { 217 | return value.getClass() == type; 218 | } 219 | 220 | @SuppressWarnings("unused") 221 | private static int get(ClassValue classValue, Object value) { 222 | return classValue.get(value.getClass()); 223 | } 224 | 225 | static MethodHandle wrapNullIfNecessary(boolean nullMatch, MethodHandle mh) { 226 | if (!nullMatch) { 227 | return mh; 228 | } 229 | return guardWithTest(NULLCHECK, 230 | dropArguments(constant(int.class, TypeSwitch.NULL_MATCH), 0, Object.class), 231 | mh); 232 | } 233 | } -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/StructuralCall.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles.Lookup; 5 | import java.lang.invoke.MethodType; 6 | 7 | /** 8 | * Allow to call methods from different classes with no common interface if they have the same name 9 | * and the same parameter types. 10 | * 11 | *

In term of performance, those calls require boxing of arguments and return value so 12 | * performance may suffer in the case the VM is not able to eliminate the boxing (sacrificing a goat 13 | * may help). 14 | * 15 | *

 16 |  * private final static StructuralCall IS_EMPTY =
 17 |  *   StructuralCall.create(MethodHandles.lookup(), "isEmpty", MethodType.methodType(boolean.class));
 18 |  *
 19 |  * static boolean isEmpty(Object o) {
 20 |  *   return IS_EMPTY.invoke(o);
 21 |  * }
 22 |  * ...
 23 |  *
 24 |  * System.out.println(isEmpty(List.of()));  // true
 25 |  * System.out.println(isEmpty(List.of(1))); // false
 26 |  * System.out.println(isEmpty(Set.of()));   // true
 27 |  * System.out.println(isEmpty(Map.of()));   // true
 28 |  * System.out.println(isEmpty(""));         // true
 29 |  * 
30 | */ 31 | public interface StructuralCall { 32 | /** 33 | * Calls the structural method with no argument. 34 | * 35 | * @param type of the return type. 36 | * @param receiver the receiver of the call. 37 | * @return the return value of the call, null if the calls return void. 38 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 39 | * same name and the same parameter types. 40 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 41 | * parameter when creating this StructuralCall. 42 | * @throws IllegalArgumentException if the structural method was created with more or less 43 | * parameters. 44 | */ 45 | R invoke(Object receiver); 46 | 47 | /** 48 | * Calls the structural method with one argument. 49 | * 50 | * @param type of the return type. 51 | * @param receiver the receiver of the call. 52 | * @param arg1 first argument. 53 | * @return the return value of the call, null if the calls return void. 54 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 55 | * same name and the same parameter types. 56 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 57 | * parameter when creating this StructuralCall. 58 | * @throws IllegalArgumentException if the structural method was created with more or less 59 | * parameters. 60 | */ 61 | R invoke(Object receiver, Object arg1); 62 | 63 | /** 64 | * Calls the structural method with two arguments. 65 | * 66 | * @param type of the return type. 67 | * @param receiver the receiver of the call. 68 | * @param arg1 first argument. 69 | * @param arg2 second argument. 70 | * @return the return value of the call, null if the calls return void. 71 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 72 | * same name and the same parameter types. 73 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 74 | * parameter when creating this StructuralCall. 75 | * @throws IllegalArgumentException if the structural method was created with more or less 76 | * parameters. 77 | */ 78 | R invoke(Object receiver, Object arg1, Object arg2); 79 | 80 | /** 81 | * Calls the structural method with three arguments. 82 | * 83 | * @param type of the return type. 84 | * @param receiver the receiver of the call. 85 | * @param arg1 first argument. 86 | * @param arg2 second argument. 87 | * @param arg3 third argument. 88 | * @return the return value of the call, null if the calls return void. 89 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 90 | * same name and the same parameter types. 91 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 92 | * parameter when creating this StructuralCall. 93 | * @throws IllegalArgumentException if the structural method was created with more or less 94 | * parameters. 95 | */ 96 | R invoke(Object receiver, Object arg1, Object arg2, Object arg3); 97 | 98 | /** 99 | * Calls the structural method with four arguments. 100 | * 101 | * @param type of the return type. 102 | * @param receiver the receiver of the call. 103 | * @param arg1 first argument. 104 | * @param arg2 second argument. 105 | * @param arg3 third argument. 106 | * @param arg4 fourth argument. 107 | * @return the return value of the call, null if the calls return void. 108 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 109 | * same name and the same parameter types. 110 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 111 | * parameter when creating this StructuralCall. 112 | * @throws IllegalArgumentException if the structural method was created with more or less 113 | * parameters. 114 | */ 115 | R invoke(Object receiver, Object arg1, Object arg2, Object arg3, Object arg4); 116 | 117 | /** 118 | * Calls the structural method with five arguments. 119 | * 120 | * @param type of the return type. 121 | * @param receiver the receiver of the call. 122 | * @param arg1 first argument. 123 | * @param arg2 second argument. 124 | * @param arg3 third argument. 125 | * @param arg4 fourth argument. 126 | * @param arg5 fifth argument. 127 | * @return the return value of the call, null if the calls return void. 128 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 129 | * same name and the same parameter types. 130 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 131 | * parameter when creating this StructuralCall. 132 | * @throws IllegalArgumentException if the structural method was created with more or less 133 | * parameters. 134 | */ 135 | R invoke(Object receiver, Object arg1, Object arg2, Object arg3, Object arg4, Object arg5); 136 | 137 | /** 138 | * Calls the structural method with six arguments. 139 | * 140 | * @param type of the return type. 141 | * @param receiver the receiver of the call. 142 | * @param arg1 first argument. 143 | * @param arg2 second argument. 144 | * @param arg3 third argument. 145 | * @param arg4 fourth argument. 146 | * @param arg5 fifth argument. 147 | * @param arg6 sixth argument. 148 | * @return the return value of the call, null if the calls return void. 149 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 150 | * same name and the same parameter types. 151 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 152 | * parameter when creating this StructuralCall. 153 | * @throws IllegalArgumentException if the structural method was created with more or less 154 | * parameters. 155 | */ 156 | R invoke( 157 | Object receiver, 158 | Object arg1, 159 | Object arg2, 160 | Object arg3, 161 | Object arg4, 162 | Object arg5, 163 | Object arg6); 164 | 165 | /** 166 | * Calls the structural method with seven arguments. 167 | * 168 | * @param type of the return type. 169 | * @param receiver the receiver of the call. 170 | * @param arg1 first argument. 171 | * @param arg2 second argument. 172 | * @param arg3 third argument. 173 | * @param arg4 fourth argument. 174 | * @param arg5 fifth argument. 175 | * @param arg6 sixth argument. 176 | * @param arg7 seventh argument. 177 | * @return the return value of the call, null if the calls return void. 178 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 179 | * same name and the same parameter types. 180 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 181 | * parameter when creating this StructuralCall. 182 | * @throws IllegalArgumentException if the structural method was created with more or less 183 | * parameters. 184 | */ 185 | R invoke( 186 | Object receiver, 187 | Object arg1, 188 | Object arg2, 189 | Object arg3, 190 | Object arg4, 191 | Object arg5, 192 | Object arg6, 193 | Object arg7); 194 | 195 | /** 196 | * Calls the structural method with eight arguments. 197 | * 198 | * @param type of the return type. 199 | * @param receiver the receiver of the call. 200 | * @param arg1 first argument. 201 | * @param arg2 second argument. 202 | * @param arg3 third argument. 203 | * @param arg4 fourth argument. 204 | * @param arg5 fifth argument. 205 | * @param arg6 sixth argument. 206 | * @param arg7 seventh argument. 207 | * @param arg8 eighth argument. 208 | * @return the return value of the call, null if the calls return void. 209 | * @throws NoSuchMethodError if the receiver and its super types doesn't declare a method with the 210 | * same name and the same parameter types. 211 | * @throws IllegalAccessError if the receiver is not accessible from the lookup that was pass as 212 | * parameter when creating this StructuralCall. 213 | * @throws IllegalArgumentException if the structural method was created with more or less 214 | * parameters. 215 | */ 216 | R invoke( 217 | Object receiver, 218 | Object arg1, 219 | Object arg2, 220 | Object arg3, 221 | Object arg4, 222 | Object arg5, 223 | Object arg6, 224 | Object arg7, 225 | Object arg8); 226 | 227 | /** 228 | * Creates a structural call that can call methods visible from the {@code lookup}, with name 229 | * {@code name} and parameter types {@code type}. 230 | * 231 | * @param lookup will be used to find the methods in the receiver classes. 232 | * @param name the name of the methods. 233 | * @param type the parameter types of the methods. 234 | * @return a new structural call that will call methods structurally. 235 | */ 236 | static StructuralCall create(Lookup lookup, String name, MethodType type) { 237 | MethodHandle mh = StructuralCallImpl.findMethodHandle(lookup, name, type); 238 | return (StructuralCallImpl) 239 | (paramCount, receiver, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) -> { 240 | try { 241 | return mh.invokeExact( 242 | paramCount, receiver, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); 243 | } catch (Throwable e) { 244 | throw Thrower.rethrow(e); 245 | } 246 | }; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/ConstantMemoizer.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.constant; 4 | import static java.lang.invoke.MethodHandles.dropArguments; 5 | import static java.lang.invoke.MethodHandles.exactInvoker; 6 | import static java.lang.invoke.MethodHandles.foldArguments; 7 | import static java.lang.invoke.MethodHandles.guardWithTest; 8 | import static java.lang.invoke.MethodHandles.lookup; 9 | import static java.lang.invoke.MethodType.methodType; 10 | 11 | import java.lang.invoke.MethodHandle; 12 | import java.lang.invoke.MethodHandles.Lookup; 13 | import java.lang.invoke.MethodType; 14 | import java.lang.invoke.MutableCallSite; 15 | import java.util.Objects; 16 | import java.util.function.Function; 17 | import java.util.function.ToDoubleFunction; 18 | import java.util.function.ToIntFunction; 19 | import java.util.function.ToLongFunction; 20 | 21 | /** 22 | * Allow to see the return value of a function as a constant in case there are few possible pairs of 23 | * key/value. 24 | * 25 | *

The method {@link #memoizer(Function, Class, Class)} returns a general purpose memoizer while 26 | * the methods {@link #intMemoizer(ToIntFunction, Class)}, {@link #longMemoizer(ToLongFunction, 27 | * Class)} and {@link #doubleMemoizer(ToDoubleFunction, Class)} returns memoizers specialized if the 28 | * value is an int, a long or a double (respectively). 29 | * 30 | *

Here is an example of usage 31 | * 32 | *

 33 |  *  enum Level {
 34 |  *    LOW, HIGH
 35 |  *  }
 36 |  *  ...
 37 |  *  private static final ToIntFunction<Level> MEMOIZER = ConstantMemoizer.intMemoizer(Level::ordinal, Level.class);
 38 |  *  ...
 39 |  *  int result = MEMOIZER.applyAsInt(Level.LOW));  // if this line is called several times,
 40 |  *                                                 // the result will be considered as constant.
 41 |  *  
42 | */ 43 | public final class ConstantMemoizer { 44 | private ConstantMemoizer() { 45 | throw new AssertionError(); 46 | } 47 | 48 | /** 49 | * Return a function that returns a constant value (for the Virtual Machine) for each key taken as 50 | * argument. The value corresponding to a key is calculated by calling the {@code function} once 51 | * by key and then cached in a code similar to a cascade of {@code if equals else}. 52 | * 53 | *

To find if a key was previously seen or not, {@link Object#equals(Object)} will be called to 54 | * compare the actual key with possibly all the keys already seen, so if there are a lot of 55 | * different keys, the performance in the worst case is like a linear search i.e. O(number of seen 56 | * keys). 57 | * 58 | * @param type of the keys. 59 | * @param type of the values. 60 | * @param function a function that takes a non null key as argument and return a non null value. 61 | * @param keyClass the class of the key, if it's a primitive type, the key value will be boxed 62 | * before calling the {@code function}. 63 | * @param valueClass the class of the value, if it's a primitive type, the value will be boxed at 64 | * each call. 65 | * @return a function the function getting the value for a specific key. 66 | * @throws NullPointerException if the {@code function}, the {@code keyClass} or the {@code 67 | * valueClass} is null, or if the function key or the function value is null. 68 | * @throws ClassCastException if the function key or the function value types doesn't match the 69 | * {@code keyClass} or the {@code valueClass}. 70 | */ 71 | public static Function memoizer( 72 | Function function, Class keyClass, Class valueClass) { 73 | Objects.requireNonNull(function); 74 | Objects.requireNonNull(keyClass); 75 | Objects.requireNonNull(valueClass); 76 | MethodHandle mh = 77 | new InliningCacheCallSite<>(methodType(valueClass, keyClass), function) 78 | .dynamicInvoker() 79 | .asType(methodType(Object.class, Object.class)); // erase 80 | return key -> { 81 | Objects.requireNonNull(key); 82 | try { 83 | return (V) mh.invokeExact(key); 84 | } catch (Throwable e) { 85 | throw Thrower.rethrow(e); 86 | } 87 | }; 88 | } 89 | 90 | /** 91 | * Return a function that returns a constant value (for the Virtual Machine) for each key taken as 92 | * argument. The value corresponding to a key is calculated by calling the {@code function} once 93 | * by key and then cached in a code similar to a cascade of {@code if equals else}. 94 | * 95 | *

To find if a key was previously seen or not, {@link Object#equals(Object)} will be called to 96 | * compare the actual key with possibly all the keys already seen, so if there are a lot of 97 | * different keys, the performance in the worst case is like a linear search i.e. O(number of seen 98 | * keys). 99 | * 100 | * @param type of the keys. 101 | * @param function a function that takes a non null key as argument and return a non null value. 102 | * @param keyClass the class of the key, if it's a primitive type, the key value will be boxed 103 | * before calling the {@code function}. 104 | * @return a function the function getting the value for a specific key. 105 | * @throws NullPointerException if the {@code function}, the {@code keyClass} is null, or if the 106 | * function key. 107 | * @throws ClassCastException if the function key types doesn't match the {@code keyClass}. 108 | */ 109 | public static ToIntFunction intMemoizer( 110 | ToIntFunction function, Class keyClass) { 111 | Objects.requireNonNull(function); 112 | Objects.requireNonNull(keyClass); 113 | MethodHandle mh = 114 | new InliningCacheCallSite<>(methodType(int.class, keyClass), function::applyAsInt) 115 | .dynamicInvoker() 116 | .asType(methodType(int.class, Object.class)); // erase 117 | return key -> { 118 | Objects.requireNonNull(key); 119 | try { 120 | return (int) mh.invokeExact(key); 121 | } catch (Throwable e) { 122 | throw Thrower.rethrow(e); 123 | } 124 | }; 125 | } 126 | 127 | /** 128 | * Return a function that returns a constant value (for the Virtual Machine) for each key taken as 129 | * argument. The value corresponding to a key is calculated by calling the {@code function} once 130 | * by key and then cached in a code similar to a cascade of {@code if equals else}. 131 | * 132 | *

To find if a key was previously seen or not, {@link Object#equals(Object)} will be called to 133 | * compare the actual key with possibly all the keys already seen, so if there are a lot of 134 | * different keys, the performance in the worst case is like a linear search i.e. O(number of seen 135 | * keys). 136 | * 137 | * @param type of the keys. 138 | * @param function a function that takes a non null key as argument and return a non null value. 139 | * @param keyClass the class of the key, if it's a primitive type, the key value will be boxed 140 | * before calling the {@code function}. 141 | * @return a function the function getting the value for a specific key. 142 | * @throws NullPointerException if the {@code function}, the {@code keyClass} is null, or if the 143 | * function key. 144 | * @throws ClassCastException if the function key types doesn't match the {@code keyClass}. 145 | */ 146 | public static ToLongFunction longMemoizer( 147 | ToLongFunction function, Class keyClass) { 148 | Objects.requireNonNull(function); 149 | Objects.requireNonNull(keyClass); 150 | MethodHandle mh = 151 | new InliningCacheCallSite<>(methodType(long.class, keyClass), function::applyAsLong) 152 | .dynamicInvoker() 153 | .asType(methodType(long.class, Object.class)); // erase 154 | return key -> { 155 | Objects.requireNonNull(key); 156 | try { 157 | return (long) mh.invokeExact(key); 158 | } catch (Throwable e) { 159 | throw Thrower.rethrow(e); 160 | } 161 | }; 162 | } 163 | 164 | /** 165 | * Return a function that returns a constant value (for the Virtual Machine) for each key taken as 166 | * argument. The value corresponding to a key is calculated by calling the {@code function} once 167 | * by key and then cached in a code similar to a cascade of {@code if equals else}. 168 | * 169 | *

To find if a key was previously seen or not, {@link Object#equals(Object)} will be called to 170 | * compare the actual key with possibly all the keys already seen, so if there are a lot of 171 | * different keys, the performance in the worst case is like a linear search i.e. O(number of seen 172 | * keys). 173 | * 174 | * @param type of the keys. 175 | * @param function a function that takes a non null key as argument and return a non null value. 176 | * @param keyClass the class of the key, if it's a primitive type, the key value will be boxed 177 | * before calling the {@code function}. 178 | * @return a function the function getting the value for a specific key. 179 | * @throws NullPointerException if the {@code function}, the {@code keyClass} is null, or if the 180 | * function key. 181 | * @throws ClassCastException if the function key types doesn't match the {@code keyClass}. 182 | */ 183 | public static ToDoubleFunction doubleMemoizer( 184 | ToDoubleFunction function, Class keyClass) { 185 | Objects.requireNonNull(function); 186 | Objects.requireNonNull(keyClass); 187 | MethodHandle mh = 188 | new InliningCacheCallSite<>(methodType(double.class, keyClass), function::applyAsDouble) 189 | .dynamicInvoker() 190 | .asType(methodType(double.class, Object.class)); // erase 191 | return key -> { 192 | Objects.requireNonNull(key); 193 | try { 194 | return (double) mh.invokeExact(key); 195 | } catch (Throwable e) { 196 | throw Thrower.rethrow(e); 197 | } 198 | }; 199 | } 200 | 201 | private static class InliningCacheCallSite extends MutableCallSite { 202 | private static final MethodHandle FALLBACK, EQUALS; 203 | 204 | static { 205 | Lookup lookup = lookup(); 206 | try { 207 | FALLBACK = 208 | lookup.findVirtual( 209 | InliningCacheCallSite.class, 210 | "fallback", 211 | methodType(MethodHandle.class, Object.class)); 212 | EQUALS = 213 | lookup.findVirtual(Object.class, "equals", methodType(boolean.class, Object.class)); 214 | } catch (NoSuchMethodException | IllegalAccessException e) { 215 | throw new AssertionError(e); 216 | } 217 | } 218 | 219 | private final Function function; 220 | 221 | InliningCacheCallSite(MethodType type, Function function) { 222 | super(type); 223 | this.function = function; 224 | setTarget( 225 | foldArguments( 226 | exactInvoker(type), 227 | FALLBACK.bindTo(this).asType(methodType(MethodHandle.class, type.parameterType(0))))); 228 | } 229 | 230 | @SuppressWarnings("unused") 231 | private MethodHandle fallback(K key) { 232 | V value = Objects.requireNonNull(function.apply(key)); 233 | MethodType type = type(); 234 | Class keyClass = type.parameterType(0); 235 | Class valueClass = type.returnType(); 236 | MethodHandle target = dropArguments(constant(valueClass, value), 0, keyClass); 237 | setTarget( 238 | guardWithTest( 239 | EQUALS.bindTo(key).asType(methodType(boolean.class, keyClass)), 240 | target, 241 | new InliningCacheCallSite<>(type, function).dynamicInvoker())); 242 | return target; 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/StableFieldTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static java.lang.invoke.MethodHandles.publicLookup; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.junit.jupiter.api.Assertions.assertNull; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import java.util.function.Function; 12 | import java.util.function.ToDoubleFunction; 13 | import java.util.function.ToIntFunction; 14 | import java.util.function.ToLongFunction; 15 | 16 | import org.junit.jupiter.api.Test; 17 | 18 | @SuppressWarnings("static-method") 19 | public class StableFieldTests { 20 | static class A { 21 | String x; 22 | } 23 | 24 | @Test 25 | public void testObjectFieldUninitialized() { 26 | Function xField = StableField.getter(lookup(), A.class, "x", String.class); 27 | A a = new A(); 28 | assertNull(xField.apply(a)); 29 | assertNull(xField.apply(a)); 30 | } 31 | 32 | @Test 33 | public void testObjectFieldStable() { 34 | Function xField = StableField.getter(lookup(), A.class, "x", String.class); 35 | A a = new A(); 36 | assertNull(xField.apply(a)); 37 | a.x = "hello"; 38 | assertEquals("hello", xField.apply(a)); 39 | assertEquals("hello", xField.apply(a)); 40 | } 41 | 42 | @Test 43 | public void testObjectFieldStableStill() { 44 | Function xField = StableField.getter(lookup(), A.class, "x", String.class); 45 | A a = new A(); 46 | assertNull(xField.apply(a)); 47 | a.x = "hello"; 48 | assertEquals("hello", xField.apply(a)); 49 | a.x = "banzai"; 50 | assertEquals("hello", xField.apply(a)); 51 | } 52 | 53 | @Test 54 | public void testObjectFieldNonConstant() { 55 | Function xField = StableField.getter(lookup(), A.class, "x", String.class); 56 | A a1 = new A(); 57 | a1.x = "foo"; 58 | assertEquals("foo", xField.apply(a1)); 59 | A a2 = new A(); 60 | assertThrows(IllegalStateException.class, () -> xField.apply(a2)); 61 | } 62 | 63 | static class B { 64 | int y; 65 | } 66 | 67 | @Test 68 | public void testIntFieldUninitialized() { 69 | ToIntFunction yField = StableField.intGetter(lookup(), B.class, "y"); 70 | B b = new B(); 71 | assertEquals(0, yField.applyAsInt(b)); 72 | assertEquals(0, yField.applyAsInt(b)); 73 | } 74 | 75 | @Test 76 | public void testIntFieldStable() { 77 | ToIntFunction yField = StableField.intGetter(lookup(), B.class, "y"); 78 | B b = new B(); 79 | assertEquals(0, yField.applyAsInt(b)); 80 | b.y = 42; 81 | assertEquals(42, yField.applyAsInt(b)); 82 | assertEquals(42, yField.applyAsInt(b)); 83 | } 84 | 85 | @Test 86 | public void testIntFieldStableStill() { 87 | ToIntFunction yField = StableField.intGetter(lookup(), B.class, "y"); 88 | B b = new B(); 89 | assertEquals(0, yField.applyAsInt(b)); 90 | b.y = 42; 91 | assertEquals(42, yField.applyAsInt(b)); 92 | b.y = 777; 93 | assertEquals(42, yField.applyAsInt(b)); 94 | } 95 | 96 | @Test 97 | public void testIntFieldNonConstant() { 98 | ToIntFunction yField = StableField.intGetter(lookup(), B.class, "y"); 99 | B b1 = new B(); 100 | b1.y = 666; 101 | assertEquals(666, yField.applyAsInt(b1)); 102 | B b2 = new B(); 103 | assertThrows(IllegalStateException.class, () -> yField.applyAsInt(b2)); 104 | } 105 | 106 | static class C { 107 | long z; 108 | } 109 | 110 | @Test 111 | public void testLongFieldUninitialized() { 112 | ToLongFunction zField = StableField.longGetter(lookup(), C.class, "z"); 113 | C c = new C(); 114 | assertEquals(0, zField.applyAsLong(c)); 115 | assertEquals(0, zField.applyAsLong(c)); 116 | } 117 | 118 | @Test 119 | public void testLongFieldStable() { 120 | ToLongFunction zField = StableField.longGetter(lookup(), C.class, "z"); 121 | C c = new C(); 122 | assertEquals(0L, zField.applyAsLong(c)); 123 | c.z = 42L; 124 | assertEquals(42L, zField.applyAsLong(c)); 125 | assertEquals(42L, zField.applyAsLong(c)); 126 | } 127 | 128 | @Test 129 | public void testLongFieldStableStill() { 130 | ToLongFunction zField = StableField.longGetter(lookup(), C.class, "z"); 131 | C c = new C(); 132 | assertEquals(0, zField.applyAsLong(c)); 133 | c.z = 42L; 134 | assertEquals(42L, zField.applyAsLong(c)); 135 | c.z = 777L; 136 | assertEquals(42L, zField.applyAsLong(c)); 137 | } 138 | 139 | @Test 140 | public void tesLongFieldNonConstant() { 141 | ToLongFunction zField = StableField.longGetter(lookup(), C.class, "z"); 142 | C c1 = new C(); 143 | c1.z = 666; 144 | assertEquals(666, zField.applyAsLong(c1)); 145 | C c2 = new C(); 146 | assertThrows(IllegalStateException.class, () -> zField.applyAsLong(c2)); 147 | } 148 | 149 | static class D { 150 | double z; 151 | } 152 | 153 | @Test 154 | public void testDoubleFieldUninitialized() { 155 | ToDoubleFunction zField = StableField.doubleGetter(lookup(), D.class, "z"); 156 | D d = new D(); 157 | assertEquals(0.0, zField.applyAsDouble(d)); 158 | assertEquals(0.0, zField.applyAsDouble(d)); 159 | } 160 | 161 | @Test 162 | public void testDoubleFieldStable() { 163 | ToDoubleFunction zField = StableField.doubleGetter(lookup(), D.class, "z"); 164 | D d = new D(); 165 | assertEquals(0.0, zField.applyAsDouble(d)); 166 | d.z = 42.0; 167 | assertEquals(42.0, zField.applyAsDouble(d)); 168 | assertEquals(42.0, zField.applyAsDouble(d)); 169 | } 170 | 171 | @Test 172 | public void testDoubleFieldStableStill() { 173 | ToDoubleFunction zField = StableField.doubleGetter(lookup(), D.class, "z"); 174 | D d = new D(); 175 | assertEquals(0.0, zField.applyAsDouble(d)); 176 | d.z = 42; 177 | assertEquals(42.0, zField.applyAsDouble(d)); 178 | d.z = 777; 179 | assertEquals(42.0, zField.applyAsDouble(d)); 180 | } 181 | 182 | @Test 183 | public void testDoubleFieldNonConstant() { 184 | ToDoubleFunction zField = StableField.doubleGetter(lookup(), D.class, "z"); 185 | D d1 = new D(); 186 | d1.z = 666.0; 187 | assertEquals(666.0, zField.applyAsDouble(d1)); 188 | D d2 = new D(); 189 | assertThrows(IllegalStateException.class, () -> zField.applyAsDouble(d2)); 190 | } 191 | 192 | static class Boxed { 193 | int i; 194 | long j; 195 | double d; 196 | } 197 | 198 | @Test 199 | public void testBoxedIntField() { 200 | Function iField = StableField.getter(lookup(), Boxed.class, "i", int.class); 201 | Boxed boxed = new Boxed(); 202 | assertEquals(0, (int) iField.apply(boxed)); 203 | boxed.i = 3; 204 | assertEquals(3, (int) iField.apply(boxed)); 205 | boxed.i = 5; 206 | assertEquals(3, (int) iField.apply(boxed)); 207 | assertThrows(IllegalStateException.class, () -> iField.apply(new Boxed())); 208 | } 209 | 210 | @Test 211 | public void testBoxedLongField() { 212 | Function jField = StableField.getter(lookup(), Boxed.class, "j", long.class); 213 | Boxed boxed = new Boxed(); 214 | assertEquals(0L, (long) jField.apply(boxed)); 215 | boxed.j = 3L; 216 | assertEquals(3L, (long) jField.apply(boxed)); 217 | boxed.j = 5L; 218 | assertEquals(3L, (long) jField.apply(boxed)); 219 | assertThrows(IllegalStateException.class, () -> jField.apply(new Boxed())); 220 | } 221 | 222 | @Test 223 | public void testBoxedDoubleField() { 224 | Function dField = StableField.getter(lookup(), Boxed.class, "d", double.class); 225 | Boxed boxed = new Boxed(); 226 | assertEquals(0.0, (double) dField.apply(boxed)); 227 | boxed.d = 3.0; 228 | assertEquals(3.0, (double) dField.apply(boxed)); 229 | boxed.d = 5.0; 230 | assertEquals(3.0, (double) dField.apply(boxed)); 231 | assertThrows(IllegalStateException.class, () -> dField.apply(new Boxed())); 232 | } 233 | 234 | static class Prim { 235 | boolean z; 236 | byte b; 237 | char c; 238 | short s; 239 | float f; 240 | } 241 | 242 | @Test 243 | public void testPrimBooleanField() { 244 | Function zField = StableField.getter(lookup(), Prim.class, "z", boolean.class); 245 | Prim prim = new Prim(); 246 | assertFalse(zField.apply(prim)); 247 | prim.z = true; 248 | assertTrue(zField.apply(prim)); 249 | prim.z = false; 250 | assertTrue(zField.apply(prim)); 251 | assertThrows(IllegalStateException.class, () -> zField.apply(new Prim())); 252 | } 253 | 254 | @Test 255 | public void testPrimByteField() { 256 | Function bField = StableField.getter(lookup(), Prim.class, "b", byte.class); 257 | Prim prim = new Prim(); 258 | assertEquals((byte) 0, (byte) bField.apply(prim)); 259 | prim.b = 10; 260 | assertEquals((byte) 10, (byte) bField.apply(prim)); 261 | prim.b = 5; 262 | assertEquals((byte) 10, (byte) bField.apply(prim)); 263 | assertThrows(IllegalStateException.class, () -> bField.apply(new Prim())); 264 | } 265 | 266 | @Test 267 | public void testPrimCharField() { 268 | Function cField = StableField.getter(lookup(), Prim.class, "c", char.class); 269 | Prim prim = new Prim(); 270 | assertEquals((char) 0, (char) cField.apply(prim)); 271 | prim.c = 'A'; 272 | assertEquals('A', (char) cField.apply(prim)); 273 | prim.c = 'B'; 274 | assertEquals('A', (char) cField.apply(prim)); 275 | assertThrows(IllegalStateException.class, () -> cField.apply(new Prim())); 276 | } 277 | 278 | @Test 279 | public void testPrimShortField() { 280 | Function sField = StableField.getter(lookup(), Prim.class, "s", short.class); 281 | Prim prim = new Prim(); 282 | assertEquals((short) 0, (short) sField.apply(prim)); 283 | prim.s = 1_000; 284 | assertEquals((short) 1_000, (short) sField.apply(prim)); 285 | prim.s = 2_000; 286 | assertEquals((short) 1_000, (short) sField.apply(prim)); 287 | assertThrows(IllegalStateException.class, () -> sField.apply(new Prim())); 288 | } 289 | 290 | @Test 291 | public void testPrimFloatField() { 292 | Function fField = StableField.getter(lookup(), Prim.class, "f", float.class); 293 | Prim prim = new Prim(); 294 | assertEquals(0.0f, (float) fField.apply(prim)); 295 | prim.f = 0.2f; 296 | assertEquals(0.2f, (float) fField.apply(prim)); 297 | prim.f = 0.4f; 298 | assertEquals(0.2f, (float) fField.apply(prim)); 299 | assertThrows(IllegalStateException.class, () -> fField.apply(new Prim())); 300 | } 301 | 302 | @Test 303 | public void testNoSuchField() { 304 | assertThrows( 305 | NoSuchFieldError.class, 306 | () -> StableField.getter(lookup(), Object.class, "foo", String.class)); 307 | assertThrows( 308 | NoSuchFieldError.class, () -> StableField.intGetter(lookup(), Object.class, "foo")); 309 | assertThrows( 310 | NoSuchFieldError.class, () -> StableField.longGetter(lookup(), Object.class, "foo")); 311 | assertThrows( 312 | NoSuchFieldError.class, () -> StableField.doubleGetter(lookup(), Object.class, "foo")); 313 | } 314 | 315 | private static class Foo { 316 | private @SuppressWarnings("unused") Object a; 317 | private @SuppressWarnings("unused") int b; 318 | private @SuppressWarnings("unused") long c; 319 | private @SuppressWarnings("unused") double d; 320 | } 321 | 322 | @Test 323 | public void testNoAccess() { 324 | assertThrows( 325 | IllegalAccessError.class, 326 | () -> StableField.getter(publicLookup(), Foo.class, "a", Object.class)); 327 | assertThrows( 328 | IllegalAccessError.class, () -> StableField.intGetter(publicLookup(), Foo.class, "b")); 329 | assertThrows( 330 | IllegalAccessError.class, () -> StableField.longGetter(publicLookup(), Foo.class, "c")); 331 | assertThrows( 332 | IllegalAccessError.class, () -> StableField.doubleGetter(publicLookup(), Foo.class, "d")); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/ObjectSupportLambdas.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.publicLookup; 4 | import static java.lang.invoke.MethodType.methodType; 5 | import static java.util.stream.Collectors.groupingBy; 6 | import static java.util.stream.Collectors.toMap; 7 | import static java.util.stream.IntStream.range; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.lang.invoke.MethodHandle; 12 | import java.lang.invoke.MethodHandles; 13 | import java.lang.invoke.MethodHandles.Lookup; 14 | import java.lang.invoke.MethodType; 15 | import java.lang.invoke.SerializedLambda; 16 | import java.lang.reflect.Method; 17 | import java.util.Arrays; 18 | import java.util.Map; 19 | import java.util.Set; 20 | import java.util.function.BiConsumer; 21 | 22 | class ObjectSupportLambdas { 23 | private static MethodHandle PRIVATE_LOOKUP_IN; 24 | static { 25 | Lookup lookup = publicLookup(); 26 | MethodHandle privateLookupIn; 27 | try { 28 | privateLookupIn = lookup.findStatic(MethodHandles.class, "privateLookupIn", methodType(Lookup.class, Class.class, Lookup.class)); 29 | } catch(IllegalAccessException e) { 30 | throw new AssertionError(e); 31 | } catch (@SuppressWarnings("unused") NoSuchMethodException e) { 32 | // JDK 8 33 | privateLookupIn = null; 34 | } 35 | PRIVATE_LOOKUP_IN = privateLookupIn; 36 | } 37 | 38 | private static SerializedLambda extractSerializedLambda(Object projectionFunction, Lookup lookup) { 39 | Class lambdaClass = projectionFunction.getClass(); 40 | MethodHandle writeReplace = (PRIVATE_LOOKUP_IN == null)? 41 | findWriteReplaceJava8(lambdaClass, lookup): 42 | findWriteReplaceJava9(lambdaClass, lookup); 43 | 44 | try { 45 | return (SerializedLambda)writeReplace.invoke(projectionFunction); 46 | } catch (Throwable e) { 47 | throw Thrower.rethrow(e); 48 | } 49 | } 50 | 51 | private static MethodHandle findWriteReplaceJava8(Class lambdaClass, Lookup lookup) { 52 | Method writeReplace; 53 | try { 54 | writeReplace = lambdaClass.getDeclaredMethod("writeReplace"); 55 | } catch (NoSuchMethodException e) { 56 | throw new IllegalArgumentException("the writeReplace method is not found, perhaps it's not a serializable lambad ?", e); 57 | } 58 | writeReplace.setAccessible(true); 59 | 60 | try { 61 | return lookup.unreflect(writeReplace); 62 | } catch (IllegalAccessException e) { 63 | throw new IllegalArgumentException("the lookup can not access to the method writeReplace", e); 64 | } 65 | } 66 | 67 | private static MethodHandle findWriteReplaceJava9(Class lambdaClass, Lookup lookup) { 68 | Lookup teleport; 69 | try { 70 | teleport = (Lookup)PRIVATE_LOOKUP_IN.invoke(lambdaClass, lookup); 71 | } catch (IllegalAccessException e) { 72 | throw new IllegalArgumentException("the lookup can not access to the lambda proxy class", e); 73 | } catch(Throwable e) { 74 | throw Thrower.rethrow(e); 75 | } 76 | 77 | try { 78 | return teleport.findVirtual(lambdaClass, "writeReplace", MethodType.methodType(Object.class)); 79 | } catch (NoSuchMethodException | IllegalAccessException e) { 80 | throw new IllegalArgumentException("the writeReplace method is not accessible, perhaps it's not a serializable lambad ?", e); 81 | } 82 | } 83 | 84 | 85 | static String[] extractFieldNames(Lookup lookup, Object[] projections) { 86 | SerializedLambda[] serializedLambdas = new SerializedLambda[projections.length]; 87 | for(int i = 0; i < serializedLambdas.length; i++) { 88 | try { 89 | serializedLambdas[i] = extractSerializedLambda(projections[i], lookup); 90 | } catch(IllegalArgumentException e) { 91 | throw new IllegalArgumentException("can not extract information from lambda at index " + i, e); 92 | } 93 | } 94 | 95 | return extractFieldNames(lookup.lookupClass(), serializedLambdas); 96 | } 97 | 98 | private static String methodDesc(SerializedLambda serializedLambda) { 99 | return serializedLambda.getImplMethodName() + serializedLambda.getImplMethodSignature(); 100 | } 101 | 102 | private static String[] extractFieldNames(Class lookupClass, SerializedLambda[] serializedLambdas) { 103 | Map> implMap = 104 | range(0, serializedLambdas.length).boxed().collect(groupingBy( 105 | index -> serializedLambdas[index].getImplClass(), 106 | toMap(index -> methodDesc(serializedLambdas[index]), index -> index))); 107 | String[] fieldNames = new String[serializedLambdas.length]; 108 | 109 | // find all lambda implementations and scan them 110 | implMap.forEach((implClassName, methodDescSlotMap) -> { 111 | byte[] data ; 112 | try(InputStream input = lookupClass.getResourceAsStream("/" + implClassName.replace('.', '/') + ".class")) { 113 | if (input == null) { 114 | throw new IllegalArgumentException("can not access to the bytecode of " + implClassName + " using lookup " + lookupClass.getName()); 115 | } 116 | 117 | data = readAllBytes(input); 118 | } catch(IOException e) { 119 | throw new AssertionError(e); 120 | } 121 | 122 | scanClassFile(data, methodDescSlotMap.keySet(), (methodDesc, fieldName) -> fieldNames[methodDescSlotMap.get(methodDesc)] = fieldName); 123 | }); 124 | 125 | // verify that all field names have been extracted 126 | for(int i = 0; i < fieldNames.length; i++) { 127 | if (fieldNames[i] == null) { 128 | throw new IllegalArgumentException("lambda " + i + " doesn't acces to a field name"); 129 | } 130 | } 131 | 132 | return fieldNames; 133 | } 134 | 135 | private static byte[] readAllBytes(InputStream input) throws IOException { 136 | byte[] buffer = new byte[8192]; 137 | int read; 138 | int total = 0; 139 | while((read = input.read(buffer, total, buffer.length - total)) != -1) { 140 | total += read; 141 | if (read == 0) { 142 | buffer = Arrays.copyOf(buffer, buffer.length + 8192); 143 | } 144 | } 145 | return Arrays.copyOf(buffer, total); 146 | } 147 | 148 | 149 | private static final int UTF8_TAG = 1; 150 | 151 | // size of the constant pool item depending on its tag 152 | private static int[] CONSTANT_SIZE = new int[] { 153 | 0, 154 | 0, // UTF8 155 | 0, 156 | 5, // INTEGER 157 | 5, // FLOAT 158 | 9, // LONG 159 | 9, // DOUBLE 160 | 3, // CLASS 161 | 3, // STRING 162 | 5, // FIEDREF 163 | 5, // METHODREF 164 | 5, // ITF_METHODREF 165 | 5, // NAME_AND_TYPE 166 | 0, 167 | 0, 168 | 4, // METHODHANDLE 169 | 3, // METHODTYPE 170 | 5, // CONDY 171 | 5, // INDY 172 | 3, // MODULE 173 | 3, // PACKAGE 174 | }; 175 | 176 | private static final int ALOAD_0 = 42; 177 | private static final int GETFIELD = 180; 178 | private static final int INVOKESTATIC = 184; 179 | private static final int ARETURN = 176; 180 | 181 | private static int readU2(byte[] data, int offset) { 182 | return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); 183 | } 184 | 185 | private static int readU4(byte[] data, int offset) { 186 | return ((data[offset] & 0xFF) << 24) 187 | | ((data[offset + 1] & 0xFF) << 16) 188 | | ((data[offset + 2] & 0xFF) << 8) 189 | | (data[offset + 3] & 0xFF); 190 | } 191 | 192 | private static String string(byte[] data, int[] offsets, String[] cache, int offset) { 193 | int index = readU2(data, offset); 194 | 195 | String s = cache[index]; 196 | if (s != null) { 197 | return s; 198 | } 199 | // read String item 200 | int itemOffset = offsets[index]; 201 | return cache[index] = readUTF8(data, itemOffset + 2, readU2(data, itemOffset)); 202 | } 203 | 204 | private static String readUTF8(byte[] data, int utfOffset, int utfLength) { 205 | int offset = utfOffset; 206 | int end = offset + utfLength; 207 | int stringIndex = 0; 208 | 209 | char[] buffer = new char[utfLength]; // may be bigger than necessary 210 | while (offset < end) { 211 | int b = data[offset++]; 212 | if ((b & 0x80) == 0) { 213 | buffer[stringIndex++] = (char) (b & 0x7F); 214 | } else if ((b & 0xE0) == 0xC0) { 215 | buffer[stringIndex++] = (char) (((b & 0x1F) << 6) | (data[offset++] & 0x3F)); 216 | } else { 217 | buffer[stringIndex++] = (char) (((b & 0xF) << 12) | ((data[offset++] & 0x3F) << 6) | (data[offset++] & 0x3F)); 218 | } 219 | } 220 | return new String(buffer, 0, stringIndex); 221 | } 222 | 223 | 224 | private static void scanClassFile(byte[] data, Set methodDescSet, BiConsumer consumer) { 225 | int constantsCount = readU2(data, 8); 226 | int[] offsets = new int[constantsCount]; 227 | 228 | // scan the constant pool items, find their offsets 229 | int offset = 10; 230 | for (int i = 1; i < constantsCount; i++) { 231 | offsets[i] = offset + 1; 232 | 233 | int constant = data[offset]; 234 | offset += CONSTANT_SIZE[constant]; 235 | if (constant == UTF8_TAG) { 236 | offset += 3 + readU2(data, offset + 1); 237 | } 238 | } 239 | 240 | // skip header 241 | int interfacesCount = readU2(data, offset + 6); 242 | offset += 8 + 2 * interfacesCount; 243 | 244 | // skip fields 245 | int fieldsCount = readU2(data, offset); 246 | offset += 2; 247 | for (int i = 0; i < fieldsCount; i++) { 248 | int attributesCount = readU2(data, offset + 6); 249 | offset += 8; 250 | // Skip field attributes 251 | for(int j = 0; j < attributesCount; j++) { 252 | offset += 6 + readU4(data, offset + 2); 253 | } 254 | } 255 | 256 | String[] cache = new String[constantsCount]; 257 | 258 | // scan methods 259 | int methodCount = readU2(data, offset); 260 | offset += 2; 261 | for (int i = 0; i < methodCount; i++) { 262 | 263 | String methodDesc = string(data, offsets, cache, offset + 2) + string(data, offsets, cache, offset + 4); 264 | 265 | int attributesCount = readU2(data, offset + 6); 266 | offset += 8; 267 | 268 | if (methodDescSet.contains(methodDesc)) { 269 | // scan to find the "Code" attribute 270 | for(int j = 0; j < attributesCount; j++) { 271 | String attributeName = string(data, offsets, cache, offset); 272 | if (attributeName.equals("Code")) { 273 | 274 | int codeOffset = offset + 6; 275 | int codeLength = readU4(data, codeOffset + 4); 276 | codeOffset += 8; 277 | int codeEnd = codeOffset + codeLength; 278 | 279 | String fieldName = null; 280 | 281 | loop: while(codeOffset < codeEnd) { 282 | int opcode = data[codeOffset] & 0xFF; 283 | switch(opcode) { 284 | case ALOAD_0: 285 | codeOffset++; 286 | break; 287 | case GETFIELD: { 288 | int fieldRefOffset = offsets[readU2(data, codeOffset + 1)]; 289 | int nameAndTypeOffset = offsets[readU2(data, fieldRefOffset + 2)]; 290 | fieldName = string(data, offsets, cache, nameAndTypeOffset); 291 | codeOffset += 3; 292 | break; 293 | } 294 | case INVOKESTATIC: 295 | codeOffset += 3; 296 | break; 297 | case ARETURN: 298 | codeOffset++; 299 | break; 300 | default: 301 | fieldName = null; // mark unrecognized 302 | break loop; 303 | } 304 | } 305 | 306 | if (fieldName != null) { 307 | // pattern fully recognized !! 308 | consumer.accept(methodDesc, fieldName); 309 | } 310 | } 311 | 312 | offset += 6 + readU4(data, offset + 2); 313 | } 314 | 315 | } else { 316 | // Skip method attributes 317 | for(int j = 0; j < attributesCount; j++) { 318 | offset += 6 + readU4(data, offset + 2); 319 | } 320 | } 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/test/java/com.github.forax.exotic/ObjectSupportTests.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.lookup; 4 | import static org.junit.jupiter.api.Assertions.assertAll; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import java.lang.annotation.ElementType; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | import java.lang.reflect.Field; 14 | import java.util.Arrays; 15 | 16 | import org.junit.jupiter.api.Test; 17 | 18 | 19 | @SuppressWarnings("static-method") 20 | public class ObjectSupportTests { 21 | static final class Hello { 22 | private static final ObjectSupport SUPPORT = ObjectSupport.of(lookup(), Hello.class, "name"); 23 | 24 | @SuppressWarnings("unused") 25 | private final String name; 26 | 27 | public Hello(String name) { 28 | this.name = name; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object other) { 33 | return SUPPORT.equals(this, other); 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return SUPPORT.hashCode(this); 39 | } 40 | } 41 | 42 | @Test 43 | public void testEqualsHello() { 44 | Hello hello1 = new Hello(new String("bonjour")); 45 | Hello hello2 = new Hello(new String("bonjour")); 46 | assertEquals(hello1, hello2); 47 | } 48 | 49 | @Test 50 | public void testHashCodeHello() { 51 | Hello hello = new Hello("ola"); 52 | assertEquals(1 * 31 + "ola".hashCode(), hello.hashCode()); 53 | } 54 | 55 | 56 | 57 | static final class Foo { 58 | private static final ObjectSupport SUPPORT = ObjectSupport.of(lookup(), Foo.class, "a", "b", "c", "d", "e", "f", "g", "h", "s", "o"); 59 | 60 | boolean a; 61 | byte b; 62 | short c; 63 | char d; 64 | int e; 65 | long f; 66 | float g; 67 | double h; 68 | String s; 69 | Object o; 70 | 71 | @Override 72 | public boolean equals(Object other) { 73 | return SUPPORT.equals(this, other); 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | return SUPPORT.hashCode(this); 79 | } 80 | } 81 | 82 | @Test 83 | public void testAllFieldsHashCode() { 84 | Foo foo1 = new Foo(); 85 | Foo foo2 = new Foo(); 86 | assertEquals(foo1.hashCode(), foo2.hashCode()); 87 | } 88 | 89 | @Test 90 | public void testAllFieldsHashCodeBoolean() { 91 | Foo foo1 = new Foo(); 92 | foo1.a = true; 93 | Foo foo2 = new Foo(); 94 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 95 | foo2.a = true; 96 | assertEquals(foo1.hashCode(), foo2.hashCode()); 97 | } 98 | @Test 99 | public void testAllFieldsHashCodeByte() { 100 | Foo foo1 = new Foo(); 101 | foo1.b = 1; 102 | Foo foo2 = new Foo(); 103 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 104 | foo2.b = 1; 105 | assertEquals(foo1.hashCode(), foo2.hashCode()); 106 | } 107 | @Test 108 | public void testAllFieldsHashCodeShort() { 109 | Foo foo1 = new Foo(); 110 | foo1.c = 1; 111 | Foo foo2 = new Foo(); 112 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 113 | foo2.c = 1; 114 | assertEquals(foo1.hashCode(), foo2.hashCode()); 115 | } 116 | @Test 117 | public void testAllFieldsHashCodeChar() { 118 | Foo foo1 = new Foo(); 119 | foo1.d = 1; 120 | Foo foo2 = new Foo(); 121 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 122 | foo2.d = 1; 123 | assertEquals(foo1.hashCode(), foo2.hashCode()); 124 | } 125 | @Test 126 | public void testAllFieldsHashCodeInt() { 127 | Foo foo1 = new Foo(); 128 | foo1.e = 1; 129 | Foo foo2 = new Foo(); 130 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 131 | foo2.e = 1; 132 | assertEquals(foo1.hashCode(), foo2.hashCode()); 133 | } 134 | @Test 135 | public void testAllFieldsHashCodeLong() { 136 | Foo foo1 = new Foo(); 137 | foo1.f = 1; 138 | Foo foo2 = new Foo(); 139 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 140 | foo2.f = 1; 141 | assertEquals(foo1.hashCode(), foo2.hashCode()); 142 | } 143 | @Test 144 | public void testAllFieldsHashCodeFloat() { 145 | Foo foo1 = new Foo(); 146 | foo1.g = 1; 147 | Foo foo2 = new Foo(); 148 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 149 | foo2.g = 1; 150 | assertEquals(foo1.hashCode(), foo2.hashCode()); 151 | } 152 | @Test 153 | public void testAllFieldsHashCodeDouble() { 154 | Foo foo1 = new Foo(); 155 | foo1.h = 1; 156 | Foo foo2 = new Foo(); 157 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 158 | foo2.h = 1; 159 | assertEquals(foo1.hashCode(), foo2.hashCode()); 160 | } 161 | @Test 162 | public void testAllFieldsHashCodeString() { 163 | Foo foo1 = new Foo(); 164 | foo1.s = new String("bar"); 165 | Foo foo2 = new Foo(); 166 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 167 | foo2.s = new String("bar"); 168 | assertEquals(foo1.hashCode(), foo2.hashCode()); 169 | } 170 | @Test 171 | public void testAllFieldsHashCodeObject() { 172 | Foo foo1 = new Foo(); 173 | foo1.o = Integer.valueOf(1500); 174 | Foo foo2 = new Foo(); 175 | assertNotEquals(foo1.hashCode(), foo2.hashCode()); 176 | foo2.o = Integer.valueOf(1500); 177 | assertEquals(foo1.hashCode(), foo2.hashCode()); 178 | } 179 | 180 | @Test 181 | public void testAllFieldsEquals() { 182 | Foo foo1 = new Foo(); 183 | Foo foo2 = new Foo(); 184 | assertEquals(foo1, foo2); 185 | } 186 | 187 | @Test 188 | public void testAllFieldsEqualsBoolean() { 189 | Foo foo1 = new Foo(); 190 | foo1.a = true; 191 | Foo foo2 = new Foo(); 192 | assertNotEquals(foo1, foo2); 193 | foo2.a = true; 194 | assertEquals(foo1, foo2); 195 | } 196 | @Test 197 | public void testAllFieldsEqualsByte() { 198 | Foo foo1 = new Foo(); 199 | foo1.b = 1; 200 | Foo foo2 = new Foo(); 201 | assertNotEquals(foo1, foo2); 202 | foo2.b = 1; 203 | assertEquals(foo1, foo2); 204 | } 205 | @Test 206 | public void testAllFieldsEqualsShort() { 207 | Foo foo1 = new Foo(); 208 | foo1.c = 1; 209 | Foo foo2 = new Foo(); 210 | assertNotEquals(foo1, foo2); 211 | foo2.c = 1; 212 | assertEquals(foo1, foo2); 213 | } 214 | @Test 215 | public void testAllFieldsEqualsChar() { 216 | Foo foo1 = new Foo(); 217 | foo1.d = 1; 218 | Foo foo2 = new Foo(); 219 | assertNotEquals(foo1, foo2); 220 | foo2.d = 1; 221 | assertEquals(foo1, foo2); 222 | } 223 | @Test 224 | public void testAllFieldsEqualsInt() { 225 | Foo foo1 = new Foo(); 226 | foo1.e = 1; 227 | Foo foo2 = new Foo(); 228 | assertNotEquals(foo1, foo2); 229 | foo2.e = 1; 230 | assertEquals(foo1, foo2); 231 | } 232 | @Test 233 | public void testAllFieldsEqualsLong() { 234 | Foo foo1 = new Foo(); 235 | foo1.f = 1; 236 | Foo foo2 = new Foo(); 237 | assertNotEquals(foo1, foo2); 238 | foo2.f = 1; 239 | assertEquals(foo1, foo2); 240 | } 241 | @Test 242 | public void testAllFieldsEqualsFloat() { 243 | Foo foo1 = new Foo(); 244 | foo1.g = 1; 245 | Foo foo2 = new Foo(); 246 | assertNotEquals(foo1, foo2); 247 | foo2.g = 1; 248 | assertEquals(foo1, foo2); 249 | } 250 | @Test 251 | public void testAllFieldsEqualsDouble() { 252 | Foo foo1 = new Foo(); 253 | foo1.h = 1; 254 | Foo foo2 = new Foo(); 255 | assertNotEquals(foo1, foo2); 256 | foo2.h = 1; 257 | assertEquals(foo1, foo2); 258 | } 259 | @Test 260 | public void testAllFieldsEqualsString() { 261 | Foo foo1 = new Foo(); 262 | foo1.s = new String("bar"); 263 | Foo foo2 = new Foo(); 264 | assertNotEquals(foo1, foo2); 265 | foo2.s = new String("bar"); 266 | assertEquals(foo1, foo2); 267 | } 268 | @Test 269 | public void testAllFieldsEqualsObject() { 270 | Foo foo1 = new Foo(); 271 | foo1.o = Integer.valueOf(1500); 272 | Foo foo2 = new Foo(); 273 | assertNotEquals(foo1, foo2); 274 | foo2.o = Integer.valueOf(1500); 275 | assertEquals(foo1, foo2); 276 | } 277 | 278 | static final class Empty { 279 | @SuppressWarnings("unchecked") 280 | static final ObjectSupport SUPPORT = ObjectSupport.of(lookup(), (Class)(Class)Empty.class, new String[0]); 281 | 282 | @Override 283 | public boolean equals(Object other) { 284 | return SUPPORT.equals(this, other); 285 | } 286 | 287 | @Override 288 | public int hashCode() { 289 | return SUPPORT.hashCode(this); 290 | } 291 | } 292 | 293 | 294 | @Test 295 | public void testSelfEqualsContract() { 296 | assertAll( 297 | () -> assertThrows(NullPointerException.class, () -> Empty.SUPPORT.equals(null, new Empty())), 298 | () -> assertThrows(ClassCastException.class, () -> Empty.SUPPORT.equals(new Object(), null)) 299 | ); 300 | } 301 | 302 | @Test 303 | public void testSelfHashCodeContract() { 304 | assertAll( 305 | () -> assertThrows(NullPointerException.class, () -> Empty.SUPPORT.hashCode(null)), 306 | () -> assertThrows(ClassCastException.class, () -> Empty.SUPPORT.hashCode(new Object())) 307 | ); 308 | } 309 | 310 | @Target(ElementType.FIELD) 311 | @Retention(RetentionPolicy.RUNTIME) 312 | @interface ObjectSupportField { 313 | // empty 314 | } 315 | 316 | static Field[] findAnnotatedFields(Class type) { 317 | return Arrays.stream(type.getDeclaredFields()).filter(field -> field.isAnnotationPresent(ObjectSupportField.class)).toArray(Field[]::new); 318 | } 319 | 320 | static class User { 321 | private static final ObjectSupport SUPPORT = ObjectSupport.ofReflection(lookup(), User.class, ObjectSupportTests::findAnnotatedFields); 322 | 323 | @ObjectSupportField 324 | String name; 325 | @ObjectSupportField 326 | boolean vip; 327 | 328 | public User(String name, boolean vip) { 329 | this.name = name; 330 | this.vip = vip; 331 | } 332 | 333 | @Override 334 | public boolean equals(Object other) { 335 | return SUPPORT.equals(this, other); 336 | } 337 | 338 | @Override 339 | public int hashCode() { 340 | return SUPPORT.hashCode(this); 341 | } 342 | } 343 | 344 | @Test 345 | public void testEqualsUser() { 346 | User user1 = new User("bob", true); 347 | User user2 = new User("bob", true); 348 | assertEquals(user1, user2); 349 | } 350 | @Test 351 | public void testNotEqualsUser() { 352 | User user1 = new User("bob", true); 353 | User user2 = new User("bob", false); 354 | assertNotEquals(user1, user2); 355 | } 356 | 357 | @Test 358 | public void testHashCodeUser() { 359 | User user1 = new User("bob", true); 360 | User user2 = new User("bob", true); 361 | assertEquals(user1.hashCode(), user2.hashCode()); 362 | } 363 | @Test 364 | public void testHashCodeNotEqualsUser() { 365 | User user1 = new User("bob", true); 366 | User user2 = new User("bob", false); 367 | assertNotEquals(user1.hashCode(), user2.hashCode()); 368 | } 369 | 370 | static class Point { 371 | private static final ObjectSupport SUPPORT; 372 | static { 373 | try { 374 | SUPPORT = ObjectSupport.of(lookup(), Point.class, p -> p.x, p -> p.y); 375 | } catch(Throwable t) { 376 | t.printStackTrace(); 377 | throw t; 378 | } 379 | } 380 | 381 | int x; 382 | int y; 383 | 384 | public Point(int x, int y) { 385 | this.x = x; 386 | this.y = y; 387 | } 388 | 389 | @Override 390 | public boolean equals(Object other) { 391 | return SUPPORT.equals(this, other); 392 | } 393 | @Override 394 | public int hashCode() { 395 | return SUPPORT.hashCode(this); 396 | } 397 | } 398 | 399 | @Test 400 | public void testEqualsPoint() { 401 | Point p1 = new Point(1, 2); 402 | Point p2 = new Point(1, 2); 403 | assertEquals(p1, p2); 404 | } 405 | 406 | @Test 407 | public void testHashCodePoint() { 408 | Point p1 = new Point(1, 2); 409 | Point p2 = new Point(1, 2); 410 | assertEquals(p1.hashCode(), p2.hashCode()); 411 | } 412 | 413 | static class Color { 414 | private static final ObjectSupport SUPPORT; 415 | static { 416 | try { 417 | SUPPORT = ObjectSupport.of(lookup(), Color.class, c -> c.name, c -> c.light); 418 | } catch(Throwable t) { 419 | t.printStackTrace(); 420 | throw t; 421 | } 422 | } 423 | 424 | String name; 425 | boolean light; 426 | 427 | public Color(String name, boolean light) { 428 | this.name = name; 429 | this.light = light; 430 | } 431 | 432 | @Override 433 | public boolean equals(Object other) { 434 | return SUPPORT.equals(this, other); 435 | } 436 | @Override 437 | public int hashCode() { 438 | return SUPPORT.hashCode(this); 439 | } 440 | } 441 | 442 | @Test 443 | public void testEqualsColor() { 444 | Color c1 = new Color("red", true); 445 | Color c2 = new Color("red", true); 446 | assertEquals(c1, c2); 447 | } 448 | 449 | @Test 450 | public void testHashCodeColor() { 451 | Color c1 = new Color("red", true); 452 | Color c2 = new Color("red", true); 453 | assertEquals(c1.hashCode(), c2.hashCode()); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/main/java/com.github.forax.exotic/StableField.java: -------------------------------------------------------------------------------- 1 | package com.github.forax.exotic; 2 | 3 | import static java.lang.invoke.MethodHandles.constant; 4 | import static java.lang.invoke.MethodHandles.dropArguments; 5 | import static java.lang.invoke.MethodHandles.exactInvoker; 6 | import static java.lang.invoke.MethodHandles.foldArguments; 7 | import static java.lang.invoke.MethodType.methodType; 8 | 9 | import java.lang.invoke.MethodHandle; 10 | import java.lang.invoke.MethodHandles; 11 | import java.lang.invoke.MethodHandles.Lookup; 12 | import java.lang.invoke.MutableCallSite; 13 | import java.util.Objects; 14 | import java.util.function.Function; 15 | import java.util.function.ToDoubleFunction; 16 | import java.util.function.ToIntFunction; 17 | import java.util.function.ToLongFunction; 18 | 19 | /** 20 | * A utility class that allow to create getters of class fields with a stable semantics. 21 | * 22 | *

The method {@link #getter(Lookup, Class, String, Class)} returns a general purpose getter 23 | * while the methods {@link #intGetter(Lookup, Class, String)}, {@link #longGetter(Lookup, Class, 24 | * String)} and {@link #doubleGetter(Lookup, Class, String)} returns getters specialized if the type 25 | * of the field is an int, a long or a double (respectively). 26 | * 27 | *

Here is an example of a kind of lazy initialization of a field of a singleton object using a 28 | * stable value. The returned value of {@code getCpuCount()} is a constant once initialized. 29 | * 30 | *

 31 |  * class SystemInfo {
 32 |  *   static final ToIntFunction<SystemInfo> CPU_COUNT = StableField.intGetter(lookup(), SystemInfo.class, "cpuCount");
 33 |  *   static final SystemInfo INSTANCE = new SystemInfo();
 34 |  *
 35 |  *   private SystemInfo() {
 36 |  *     // enforce singleton
 37 |  *   }
 38 |  *
 39 |  *   int cpuCount;  // stable
 40 |  *
 41 |  *   public int getCpuCount() {
 42 |  *     int cpuCount = CPU_COUNT.applyAsInt(this);
 43 |  *     if (cpuCount == 0) {
 44 |  *       return this.cpuCount = Runtime.getRuntime().availableProcessors();
 45 |  *     }
 46 |  *     return cpuCount;
 47 |  *   }
 48 |  * }
 49 |  * 
50 | * 51 | * The stable semantics is defined by the following rules: If the field is not initialized or 52 | * initialized with its default value, the default value will be returned when calling the getter. 53 | * 54 | *

If the field is initialized with another value than the default value, the getter will return 55 | * the first value of the field observed by the getter, any subsequent calls to the getter will 56 | * return this same value. 57 | * 58 | *

If the getter has observed a value different from the default value, any subsequent calls to 59 | * the getter need to pass the same object as argument of the getter. 60 | */ 61 | public final class StableField { 62 | private StableField() { 63 | throw new AssertionError(); 64 | } 65 | 66 | /** 67 | * Create a getter on a field of a class with a stable semantics. If the type of the field is a 68 | * primitive type, the value will be boxed. 69 | * 70 | *

If the field is not initialized or initialized with its default value, the default value 71 | * will be returned when calling the getter. If the field is initialized with another value than 72 | * the default value, the getter will return the first value of the field observed by the getter, 73 | * any subsequent calls to the getter will return this same value. 74 | * 75 | *

If the getter has observed a value different from the default value, any subsequent calls to 76 | * the getter need to pass the same object as argument of the getter. 77 | * 78 | * @param the type of the object containing the field. 79 | * @param the type of the field. 80 | * @param lookup a lookup object that can access to the field. 81 | * @param declaringClass the class that declares the field. 82 | * @param name the name of the field. 83 | * @param type the type of the field. 84 | * @return a function that takes an object of the {@code declaring class} and returns the value of 85 | * the field. 86 | * @throws NullPointerException if either the lookup, the declaring class, the name or the type is 87 | * null. 88 | * @throws NoSuchFieldError if the field doesn't exist. 89 | * @throws IllegalAccessError if the field is not accessible from the lookup. 90 | * @throws IllegalStateException if the argument of the getter is not constant. 91 | */ 92 | public static Function getter( 93 | Lookup lookup, Class declaringClass, String name, Class type) { 94 | Objects.requireNonNull(lookup); 95 | Objects.requireNonNull(declaringClass); 96 | Objects.requireNonNull(name); 97 | Objects.requireNonNull(type); 98 | MethodHandle getter = createGetter(lookup, declaringClass, name, type); 99 | MethodHandle mh = new StableFieldCS(getter, Object.class).dynamicInvoker(); 100 | return object -> { 101 | try { 102 | return (V) mh.invokeExact(object); 103 | } catch (Throwable t) { 104 | throw Thrower.rethrow(t); 105 | } 106 | }; 107 | } 108 | 109 | /** 110 | * Create a getter on a field of type {@code int} of a class with a stable semantics. 111 | * 112 | *

If the field is not initialized or initialized with its default value, the default value 113 | * will be returned when calling the getter. If the field is initialized with another value than 114 | * the default value, the getter will return the first value of the field observed by the getter, 115 | * any subsequent calls to the getter will return this same value. 116 | * 117 | *

If the getter has observed a value different from the default value, any subsequent calls to 118 | * the getter need to pass the same object as argument of the getter. 119 | * 120 | *

This call is equivalent to a call to {@link #getter(Lookup, Class, String, Class)} with 121 | * {@code int.class} as last argument that returns a getter that doesn't box the return value. 122 | * 123 | * @param the type of the object containing the field. 124 | * @param lookup a lookup object that can access to the field. 125 | * @param declaringClass the class that declares the field. 126 | * @param name the name of the field. 127 | * @return a function that takes an object of the {@code declaring class} and returns the value of 128 | * the field. 129 | * @throws NullPointerException if either the lookup, the declaring class or the name is null. 130 | * @throws NoSuchFieldError if the field doesn't exist. 131 | * @throws IllegalAccessError if the field is not accessible from the lookup. 132 | * @throws IllegalStateException if the argument of the getter is not constant. 133 | */ 134 | public static ToIntFunction intGetter( 135 | Lookup lookup, Class declaringClass, String name) { 136 | Objects.requireNonNull(lookup); 137 | Objects.requireNonNull(declaringClass); 138 | Objects.requireNonNull(name); 139 | MethodHandle getter = createGetter(lookup, declaringClass, name, int.class); 140 | MethodHandle mh = new StableFieldCS(getter, int.class).dynamicInvoker(); 141 | return object -> { 142 | try { 143 | return (int) mh.invokeExact(object); 144 | } catch (Throwable t) { 145 | throw Thrower.rethrow(t); 146 | } 147 | }; 148 | } 149 | 150 | /** 151 | * Create a getter on a field of type {@code long} of a class with a stable semantics. 152 | * 153 | *

If the field is not initialized or initialized with its default value, the default value 154 | * will be returned when calling the getter. If the field is initialized with another value than 155 | * the default value, the getter will return the first value of the field observed by the getter, 156 | * any subsequent calls to the getter will return this same value. 157 | * 158 | *

If the getter has observed a value different from the default value, any subsequent calls to 159 | * the getter need to pass the same object as argument of the getter. 160 | * 161 | *

This call is equivalent to a call to {@link #getter(Lookup, Class, String, Class)} with 162 | * {@code long.class} as last argument that returns a getter that doesn't box the return value. 163 | * 164 | * @param the type of the object containing the field. 165 | * @param lookup a lookup object that can access to the field. 166 | * @param declaringClass the class that declares the field. 167 | * @param name the name of the field. 168 | * @return a function that takes an object of the {@code declaring class} and returns the value of 169 | * the field. 170 | * @throws NullPointerException if either the lookup, the declaring class or the name is null. 171 | * @throws NoSuchFieldError if the field doesn't exist. 172 | * @throws IllegalAccessError if the field is not accessible from the lookup. 173 | * @throws IllegalStateException if the argument of the getter is not constant. 174 | */ 175 | public static ToLongFunction longGetter( 176 | Lookup lookup, Class declaringClass, String name) { 177 | Objects.requireNonNull(lookup); 178 | Objects.requireNonNull(declaringClass); 179 | Objects.requireNonNull(name); 180 | MethodHandle getter = createGetter(lookup, declaringClass, name, long.class); 181 | MethodHandle mh = new StableFieldCS(getter, long.class).dynamicInvoker(); 182 | return object -> { 183 | try { 184 | return (long) mh.invokeExact(object); 185 | } catch (Throwable t) { 186 | throw Thrower.rethrow(t); 187 | } 188 | }; 189 | } 190 | 191 | /** 192 | * Create a getter on a field of type {@code double} of a class with a stable semantics. 193 | * 194 | *

If the field is not initialized or initialized with its default value, the default value 195 | * will be returned when calling the getter. If the field is initialized with another value than 196 | * the default value, the getter will return the first value of the field observed by the getter, 197 | * any subsequent calls to the getter will return this same value. 198 | * 199 | *

If the getter has observed a value different from the default value, any subsequent calls to 200 | * the getter need to pass the same object as argument of the getter. 201 | * 202 | *

This call is equivalent to a call to {@link #getter(Lookup, Class, String, Class)} with 203 | * {@code double.class} as last argument that returns a getter that doesn't box the return value. 204 | * 205 | * @param the type of the object containing the field. 206 | * @param lookup a lookup object that can access to the field. 207 | * @param declaringClass the class that declares the field. 208 | * @param name the name of the field. 209 | * @return a function that takes an object of the {@code declaring class} and returns the value of 210 | * the field. 211 | * @throws NullPointerException if either the lookup, the declaring class or the name is null. 212 | * @throws NoSuchFieldError if the field doesn't exist. 213 | * @throws IllegalAccessError if the field is not accessible from the lookup. 214 | * @throws IllegalStateException if the argument of the getter is not constant. 215 | */ 216 | public static ToDoubleFunction doubleGetter( 217 | Lookup lookup, Class declaringClass, String name) { 218 | Objects.requireNonNull(lookup); 219 | Objects.requireNonNull(declaringClass); 220 | Objects.requireNonNull(name); 221 | MethodHandle getter = createGetter(lookup, declaringClass, name, double.class); 222 | MethodHandle mh = new StableFieldCS(getter, double.class).dynamicInvoker(); 223 | return object -> { 224 | try { 225 | return (double) mh.invokeExact(object); 226 | } catch (Throwable t) { 227 | throw Thrower.rethrow(t); 228 | } 229 | }; 230 | } 231 | 232 | private static MethodHandle createGetter( 233 | Lookup lookup, Class declaringClass, String name, Class type) 234 | throws NoSuchFieldError, IllegalAccessError { 235 | try { 236 | return lookup.findGetter(declaringClass, name, type); 237 | } catch (NoSuchFieldException e) { 238 | throw (NoSuchFieldError) new NoSuchFieldError().initCause(e); 239 | } catch (IllegalAccessException e) { 240 | throw (IllegalAccessError) new IllegalAccessError().initCause(e); 241 | } 242 | } 243 | 244 | private static class StableFieldCS extends MutableCallSite { 245 | private static final MethodHandle FALLBACK, VALUE_CHECK, NOT_CONSTANT; 246 | 247 | static { 248 | Lookup lookup = MethodHandles.lookup(); 249 | try { 250 | FALLBACK = 251 | lookup.findVirtual( 252 | StableFieldCS.class, "fallback", methodType(MethodHandle.class, Object.class)); 253 | VALUE_CHECK = 254 | lookup.findStatic( 255 | StableFieldCS.class, 256 | "valueCheck", 257 | methodType(boolean.class, Object.class, Object.class)); 258 | NOT_CONSTANT = 259 | lookup.findStatic( 260 | StableFieldCS.class, "notConstant", methodType(void.class, Object.class)); 261 | } catch (NoSuchMethodException | IllegalAccessException e) { 262 | throw new AssertionError(e); 263 | } 264 | } 265 | 266 | private final MethodHandle getter; 267 | 268 | StableFieldCS(MethodHandle getter, Class returnType) { 269 | super(methodType(returnType, Object.class)); 270 | this.getter = getter; 271 | setTarget(foldArguments(exactInvoker(type()), FALLBACK.bindTo(this))); 272 | } 273 | 274 | @SuppressWarnings("unused") 275 | private MethodHandle fallback(Object o) throws Throwable { 276 | Objects.requireNonNull(o); 277 | Object result = getter.invoke(o); 278 | MethodHandle constant = 279 | dropArguments(constant(getter.type().returnType(), result), 0, Object.class) 280 | .asType(type()); 281 | if (!Objects.equals(result, zero(getter.type().returnType()))) { 282 | MethodHandle target = 283 | MethodHandles.guardWithTest( 284 | VALUE_CHECK.bindTo(o), constant, NOT_CONSTANT.asType(type())); 285 | setTarget(target); 286 | } 287 | return constant; 288 | } 289 | 290 | private static Object zero(Class type) { 291 | if (type == int.class) { 292 | return 0; 293 | } 294 | if (type == long.class) { 295 | return 0L; 296 | } 297 | if (type == double.class) { 298 | return 0.0; 299 | } 300 | if (type == boolean.class) { 301 | return false; 302 | } 303 | if (type == byte.class) { 304 | return (byte) 0; 305 | } 306 | if (type == short.class) { 307 | return (short) 0; 308 | } 309 | if (type == char.class) { 310 | return (char) 0; 311 | } 312 | if (type == float.class) { 313 | return 0f; 314 | } 315 | return null; 316 | } 317 | 318 | @SuppressWarnings("unused") 319 | private static boolean valueCheck(Object v1, Object v2) { 320 | return v1 == v2; 321 | } 322 | 323 | @SuppressWarnings("unused") 324 | private static void notConstant(Object o) { 325 | throw new IllegalStateException("the receiver is not constant"); 326 | } 327 | } 328 | } 329 | --------------------------------------------------------------------------------