aot.patch
| new/build.gradle 2026-09-25 09:27:23.844791001 +0300 | ||
|---|---|---|
| 555 | 555 |
from "${layout.buildDirectory.get()}/classes"
|
| 556 | 556 |
into "${layout.buildDirectory.get()}/classes.aop"
|
| 557 | 557 |
includeEmptyDirs = false |
| 558 |
dependsOn 'classes', 'copyResources' |
|
| 558 |
dependsOn 'classes', 'copyResources', 'createCompileClasspathJar'
|
|
| 559 | 559 |
} |
| 560 | 560 | |
| 561 | 561 |
// Copy tools/scripts to build/tools/scripts/ (these go outside classes/) |
| ... | ... | |
| 786 | 786 |
} |
| 787 | 787 | |
| 788 | 788 |
// ============================================================================ |
| 789 |
// Ahead-of-time generation of the expression classes of the shipped rule sets. |
|
| 790 |
// |
|
| 791 |
// A conversion otherwise generates, defines and JIT compiles a class for every expression of every |
|
| 792 |
// rule set it loads, on every run. Those rule sets are fixed content shipped with FWD, so the work |
|
| 793 |
// is the same every time and is done here instead, once, and packaged into p2j.jar. |
|
| 794 |
// |
|
| 795 |
// Up-to-date checking is what keeps this off the critical path of an ordinary build: the task |
|
| 796 |
// reruns only when a rule file changes, or when the expression compiler which generates the classes |
|
| 797 |
// changes. The latter matters because a change to code generation alters what the correct bytecode |
|
| 798 |
// is, and nothing at run time would detect a class generated by an older compiler. |
|
| 799 |
// ============================================================================ |
|
| 800 |
// NOT yet wired into 'jar': the generator currently produces an empty index, because compiling a |
|
| 801 |
// rule set's expressions outside a running conversion does not resolve the pattern worker |
|
| 802 |
// namespaces (see AotWarmer). Wiring it in would add minutes to every build for no benefit. |
|
| 803 |
tasks.register('generateAotExpressions', JavaExec) {
|
|
| 804 |
description = 'Generate the expression classes of the shipped rule sets (experimental)' |
|
| 805 |
group = 'build' |
|
| 806 | ||
| 807 |
dependsOn 'classes', 'copyResources' |
|
| 808 | ||
| 809 |
def aotDir = layout.buildDirectory.dir('aot')
|
|
| 810 | ||
| 811 |
def aotLevel = (project.findProperty('aotDebugLevel') ?: '0').toString()
|
|
| 812 | ||
| 813 |
inputs.files(fileTree('rules'))
|
|
| 814 |
.withPropertyName('ruleSets')
|
|
| 815 |
.withPathSensitivity(PathSensitivity.RELATIVE) |
|
| 816 |
inputs.files(fileTree('src/com/goldencode/expr'))
|
|
| 817 |
.withPropertyName('expressionCompiler')
|
|
| 818 |
.withPathSensitivity(PathSensitivity.RELATIVE) |
|
| 819 |
// the level decides what the compiler emits, so it has to invalidate the output like any |
|
| 820 |
// other input; without this, changing it would leave the previous level's classes in place |
|
| 821 |
inputs.property('debugLevel', aotLevel)
|
|
| 822 |
outputs.dir(aotDir).withPropertyName('generatedClasses')
|
|
| 823 | ||
| 824 |
// The generator loads every profile, and the pattern engine has plenty to say while doing so |
|
| 825 |
// -- none of it about this task. Capture it all and surface only the one summary line, unless |
|
| 826 |
// the build is run with --info or the generator fails, in which case show everything. |
|
| 827 |
def captured = new ByteArrayOutputStream() |
|
| 828 | ||
| 829 |
standardOutput = captured |
|
| 830 |
errorOutput = captured |
|
| 831 |
ignoreExitValue = true |
|
| 832 | ||
| 833 |
mainClass = 'com.goldencode.p2j.pattern.AotWarmer' |
|
| 834 |
// compileJava writes to build/classes, and the dependency set is reached through the manifest |
|
| 835 |
// classpath jar the build already assembles for exactly this purpose |
|
| 836 |
classpath = files("${layout.buildDirectory.get()}/classes",
|
|
| 837 |
"${layout.buildDirectory.get()}/compile-classpath.jar")
|
|
| 838 |
.builtBy('classes', 'copyResources', 'createCompileClasspathJar')
|
|
| 839 |
maxHeapSize = '4g' |
|
| 840 | ||
| 841 |
doFirst {
|
|
| 842 |
// 0..3 only: see DebugLevels. A value outside that range silently falls through to no |
|
| 843 |
// runtime flags, producing level 0 classes labelled as something else, which is worse than |
|
| 844 |
// a failure. |
|
| 845 |
if (!(aotLevel ==~ /[0-3]/)) {
|
|
| 846 |
throw new GradleException( |
|
| 847 |
"aotDebugLevel must be 0..3 (none, status, debug, trace), got '${aotLevel}'")
|
|
| 848 |
} |
|
| 849 | ||
| 850 |
// The pattern engine expects a project home with a cfg/ directory. Generation needs none |
|
| 851 |
// of what a real project configures -- no schema, no file set, no output root -- because |
|
| 852 |
// rule set variables carry explicit Java types and pattern worker libraries are plain Java |
|
| 853 |
// classes. So a minimal home is synthesized here, pointing only at the rules to load. |
|
| 854 |
def aotHome = layout.buildDirectory.dir('aot-home').get().asFile
|
|
| 855 |
def cfgDir = new File(aotHome, 'cfg') |
|
| 856 | ||
| 857 |
cfgDir.mkdirs() |
|
| 858 | ||
| 859 |
new File(cfgDir, 'p2j.cfg.xml').text = """<?xml version="1.0"?> |
|
| 860 |
<!-- Generated by the generateAotExpressions task; not a project configuration. --> |
|
| 861 |
<cfg> |
|
| 862 |
<global> |
|
| 863 |
<parameter name="P2J_RULES" value="${projectDir.path}/rules" />
|
|
| 864 |
<parameter name="patpath" value=".:\${P2J_RULES}/include:\${P2J_RULES}:" />
|
|
| 865 |
</global> |
|
| 866 |
<profile name="default" default="true"> |
|
| 867 |
<file-set/> |
|
| 868 |
</profile> |
|
| 869 |
<schema> |
|
| 870 |
<metadata name="standard"/> |
|
| 871 |
</schema> |
|
| 872 |
</cfg> |
|
| 873 |
""" |
|
| 874 | ||
| 875 |
workingDir = aotHome |
|
| 876 |
systemProperty 'P2J_HOME', aotHome.path |
|
| 877 | ||
| 878 |
// Must match the Dn debug level conversions run at (see the -SIdN argument in a project's |
|
| 879 |
// build.xml). Override with -PaotDebugLevel=N. |
|
| 880 |
systemProperty 'p2j.aot.debug-level', aotLevel |
|
| 881 |
systemProperty 'p2j.aot.verbose', (project.findProperty('aotVerbose') ?: 'false')
|
|
| 882 | ||
| 883 |
// every profile in the rules tree; one which cannot be loaded is reported and skipped, |
|
| 884 |
// and its expressions simply keep being compiled at conversion time |
|
| 885 |
// Every profile in the rules tree except the call graph ones, which are a separate |
|
| 886 |
// pipeline and bring up their JanusGraph/BerkeleyDB store merely to be loaded. |
|
| 887 |
// |
|
| 888 |
// Resist pruning further by directory name. The profiles under runtime/ are loaded by |
|
| 889 |
// the *server*, not by a conversion -- it converts dynamic queries through this same |
|
| 890 |
// pattern engine, see ConversionProfile in the persist package -- and dropping them on |
|
| 891 |
// the assumption that "runtime" meant something else silently cost the server its |
|
| 892 |
// generated classes. progress/ and reports/ are likewise reached, from |
|
| 893 |
// ProgressTransformDriver and ReportDriver. A profile which fails to load is reported |
|
| 894 |
// and skipped anyway, so guessing which ones matter buys nothing. |
|
| 895 |
def excluded = ['callgraph/'] |
|
| 896 | ||
| 897 |
def profiles = fileTree('rules') { include '**/*.xml' }
|
|
| 898 |
.files |
|
| 899 |
.collect { file(it).path - (projectDir.path + '/rules/') - '.xml' }
|
|
| 900 |
.findAll { name -> !excluded.any { name.startsWith(it) } }
|
|
| 901 |
.sort() |
|
| 902 | ||
| 903 |
logger.info("generateAotExpressions: ${profiles.size()} profiles")
|
|
| 904 | ||
| 905 |
args = [aotDir.get().asFile.path] + profiles |
|
| 906 |
} |
|
| 907 | ||
| 908 |
doLast {
|
|
| 909 |
def text = captured.toString() |
|
| 910 | ||
| 911 |
if (executionResult.get().exitValue != 0) {
|
|
| 912 |
logger.error(text) |
|
| 913 | ||
| 914 |
throw new GradleException('generateAotExpressions failed')
|
|
| 915 |
} |
|
| 916 | ||
| 917 |
if (logger.isInfoEnabled()) {
|
|
| 918 |
logger.lifecycle(text.trim()) |
|
| 919 |
} |
|
| 920 |
else {
|
|
| 921 |
text.readLines() |
|
| 922 |
.findAll { it.startsWith('AOT expressions:') }
|
|
| 923 |
.each { logger.lifecycle(it) }
|
|
| 924 |
} |
|
| 925 |
} |
|
| 926 |
} |
|
| 927 | ||
| 928 |
// Identity of the inputs the generated expression classes were produced from. The same value is |
|
| 929 |
// written into both jars; a conversion compares them and ignores the generated classes unless |
|
| 930 |
// they agree, so a fwd-trpl.jar left behind from an older build is inert rather than wrong. |
|
| 931 |
def aotStampValue = {
|
|
| 932 |
def digest = java.security.MessageDigest.getInstance('SHA-256')
|
|
| 933 |
def sources = [] |
|
| 934 | ||
| 935 |
fileTree('rules').files.each { sources << it }
|
|
| 936 |
fileTree('src/com/goldencode/expr').files.each { sources << it }
|
|
| 937 | ||
| 938 |
sources.sort { it.path }.each { f ->
|
|
| 939 |
digest.update((f.path - projectDir.path).getBytes('UTF-8'))
|
|
| 940 |
digest.update(java.security.MessageDigest.getInstance('SHA-256').digest(f.bytes))
|
|
| 941 |
} |
|
| 942 | ||
| 943 |
return digest.digest().encodeHex().toString().substring(0, 32) |
|
| 944 |
} |
|
| 945 | ||
| 946 |
tasks.register('aotStamp') {
|
|
| 947 |
description = 'Compute the identity shared by p2j.jar and fwd-trpl.jar' |
|
| 948 | ||
| 949 |
// part of src/com/goldencode/expr is generated, so the generators have to have run before |
|
| 950 |
// its content can be hashed |
|
| 951 |
dependsOn 'classes' |
|
| 952 | ||
| 953 |
inputs.files(fileTree('rules')).withPropertyName('ruleSets')
|
|
| 954 |
inputs.files(fileTree('src/com/goldencode/expr')).withPropertyName('expressionCompiler')
|
|
| 955 |
outputs.dir(layout.buildDirectory.dir('aot-stamp')).withPropertyName('stamp')
|
|
| 956 | ||
| 957 |
doLast {
|
|
| 958 |
def value = aotStampValue() |
|
| 959 | ||
| 960 |
// the copy inside p2j.jar: what a conversion expects the generated classes to have been |
|
| 961 |
// built from |
|
| 962 |
def expected = layout.buildDirectory.file('aot-stamp/com/goldencode/expr/aot-expected.txt')
|
|
| 963 |
.get().asFile |
|
| 964 | ||
| 965 |
expected.parentFile.mkdirs() |
|
| 966 |
expected.text = value + '\n' |
|
| 967 | ||
| 968 |
logger.info("aotStamp: ${value}")
|
|
| 969 |
} |
|
| 970 |
} |
|
| 971 | ||
| 972 |
// The generated expression classes, packaged apart from p2j.jar. Separate because there are tens |
|
| 973 |
// of thousands of them: folding them into p2j.jar pushed it past the 64k zip entry limit, and it |
|
| 974 |
// keeps an optional, regenerable artifact out of the one that has to be right. |
|
| 975 |
tasks.register('trplJar', Jar) {
|
|
| 976 |
dependsOn 'generateAotExpressions', 'aotStamp' |
|
| 977 |
archiveFileName = 'fwd-trpl.jar' |
|
| 978 |
destinationDirectory = file("${layout.buildDirectory.get()}/lib")
|
|
| 979 | ||
| 980 |
from("${layout.buildDirectory.get()}/aot") {
|
|
| 981 |
include 'com/goldencode/expr/aot/**' |
|
| 982 |
} |
|
| 983 | ||
| 984 |
doFirst {
|
|
| 985 |
// the copy inside fwd-trpl.jar: what these classes were actually built from |
|
| 986 |
def stampFile = layout.buildDirectory.file('aot/com/goldencode/expr/aot/stamp.txt')
|
|
| 987 |
.get().asFile |
|
| 988 | ||
| 989 |
stampFile.parentFile.mkdirs() |
|
| 990 |
stampFile.text = aotStampValue() + '\n' |
|
| 991 |
} |
|
| 992 |
} |
|
| 993 | ||
| 994 | ||
| 995 |
// ============================================================================ |
|
| 789 | 996 |
// JAR creation |
| 790 | 997 |
// Three JARs from build/classes.aop/: p2j.jar, fwdspi.jar, fwdaopltw.jar |
| 791 | 998 |
// Plus p2jpl.jar for PL/Java |
| ... | ... | |
| 796 | 1003 |
dependsOn 'prepareClassesAop', 'makeManifests', 'copyDependencies', |
| 797 | 1004 |
'mergePostgreSQLUDFs', 'mergeMariaDBUDFs', 'mergeSqlServerUDFs', |
| 798 | 1005 |
'copyToolsScripts', 'fwdspiJar', 'fwdaopltwJar', |
| 799 |
'createCompileClasspathJar', 'createTestsClasspathJar' |
|
| 1006 |
'createCompileClasspathJar', 'createTestsClasspathJar', |
|
| 1007 |
'trplJar', 'aotStamp' |
|
| 800 | 1008 |
archiveFileName = 'p2j.jar' |
| 801 | 1009 |
destinationDirectory = file("${layout.buildDirectory.get()}/lib")
|
| 802 | 1010 |
from("${layout.buildDirectory.get()}/classes.aop") {
|
| ... | ... | |
| 824 | 1032 |
from("${layout.buildDirectory.get()}/admin") {
|
| 825 | 1033 |
include 'com/goldencode/p2j/admin/**' |
| 826 | 1034 |
} |
| 1035 |
// Only the expected stamp; the generated classes themselves live in fwd-trpl.jar. |
|
| 1036 |
from("${layout.buildDirectory.get()}/aot-stamp") {
|
|
| 1037 |
include 'com/goldencode/expr/aot-expected.txt' |
|
| 1038 |
} |
|
| 1039 | ||
| 827 | 1040 |
// Include the empty SPI directory to match reference |
| 828 | 1041 |
from("${layout.buildDirectory.get()}/classes.aop") {
|
| 829 | 1042 |
include 'com/goldencode/p2j/spi/' |
| ... | ... | |
| 854 | 1067 |
attributes( |
| 855 | 1068 |
'Implementation-Version': ant.properties['fwdver'], |
| 856 | 1069 |
'Main-Class': 'com.goldencode.p2j.Version', |
| 857 |
'Class-Path': configurations.fwdAllRuntime.collect { it.name }.join(' ')
|
|
| 1070 |
// fwd-trpl.jar carries the expression classes generated from the shipped rule |
|
| 1071 |
// sets. Referenced here so that a project picks it up without touching its own |
|
| 1072 |
// build, and optional at run time: if it is absent or stale, expressions are |
|
| 1073 |
// compiled exactly as they always were. |
|
| 1074 |
'Class-Path': (['fwd-trpl.jar'] + |
|
| 1075 |
configurations.fwdAllRuntime.collect { it.name }).join(' ')
|
|
| 858 | 1076 |
) |
| 859 | 1077 |
} |
| 860 | 1078 |
} |
| ... | ... | |
| 1097 | 1315 |
destinationDirectory = file(distDir) |
| 1098 | 1316 |
from("${layout.buildDirectory.get()}/lib") {
|
| 1099 | 1317 |
into 'lib' |
| 1100 |
include 'p2j.jar', 'fwdaopltw.jar', 'fwd_sheet.war', libname |
|
| 1318 |
// fwd-trpl.jar is needed here as well as for conversion: the server converts dynamic |
|
| 1319 |
// queries at runtime through the same pattern engine (see ConversionProfile). |
|
| 1320 |
include 'p2j.jar', 'fwdaopltw.jar', 'fwd-trpl.jar', 'fwd_sheet.war', libname |
|
| 1101 | 1321 |
} |
| 1102 | 1322 |
from("${layout.buildDirectory.get()}/lib") {
|
| 1103 | 1323 |
into 'lib/spi' |
| new/src/com/goldencode/expr/AotGenerator.java 2026-09-25 09:27:23.846000306 +0300 | ||
|---|---|---|
| 1 |
/* |
|
| 2 |
** Module : AotGenerator.java |
|
| 3 |
** Abstract : Build time generation of the expression classes of the shipped rule sets |
|
| 4 |
** |
|
| 5 |
** Copyright (c) 2026, Golden Code Development Corporation. |
|
| 6 |
** |
|
| 7 |
** -#- -I- --Date-- ----------------Description----------------- |
|
| 8 |
** 001 TG 20260909 Initial version. |
|
| 9 |
*/ |
|
| 10 | ||
| 11 |
package com.goldencode.expr; |
|
| 12 | ||
| 13 |
import java.io.*; |
|
| 14 |
import java.nio.charset.*; |
|
| 15 |
import java.nio.file.*; |
|
| 16 |
import java.security.*; |
|
| 17 |
import java.util.*; |
|
| 18 | ||
| 19 |
/** |
|
| 20 |
* Collects the expression classes of the shipped rule sets at build time, so that a conversion can |
|
| 21 |
* load them instead of generating them again on every run. |
|
| 22 |
* |
|
| 23 |
* <h3>How generation is driven</h3> |
|
| 24 |
* |
|
| 25 |
* This class does not walk the rule sets itself. It switches the compiler into generation mode and |
|
| 26 |
* lets the normal machinery run: a driver loads each profile and forces every expression in it to |
|
| 27 |
* compile, and each resulting class arrives here through {@link #emit}. That keeps one code path
|
|
| 28 |
* for producing a class, which is what makes a generated class trustworthy -- there is no second |
|
| 29 |
* implementation to drift from the first. |
|
| 30 |
* |
|
| 31 |
* <h3>Naming and sharing</h3> |
|
| 32 |
* |
|
| 33 |
* Classes are named from {@link Compiler#signature}, a description of the compilation which is
|
|
| 34 |
* available before any code is assembled and which is verified to determine the emitted bytecode |
|
| 35 |
* exactly. Occurrences whose compilations are interchangeable therefore land on one name and are |
|
| 36 |
* emitted once, which is why the class count is well below the occurrence count. |
|
| 37 |
* <p> |
|
| 38 |
* The names live in their own package, so they can never collide with the sequentially named |
|
| 39 |
* classes an ordinary compilation produces. |
|
| 40 |
* |
|
| 41 |
* @see AotIndex |
|
| 42 |
*/ |
|
| 43 |
public final class AotGenerator |
|
| 44 |
{
|
|
| 45 |
/** Package holding every generated class. */ |
|
| 46 |
static final String PACKAGE = AotIndex.PACKAGE; |
|
| 47 | ||
| 48 |
/** Flag indicating the compiler should name and emit classes for the index. */ |
|
| 49 |
private static volatile boolean generating = false; |
|
| 50 | ||
| 51 |
/** Emitted classes, keyed by fully qualified name, so a shared class is written once. */ |
|
| 52 |
private static final Map<String, byte[]> classes = new TreeMap<>(); |
|
| 53 | ||
| 54 |
/** Index entries, keyed by expression occurrence name. */ |
|
| 55 |
private static final Map<String, String> index = new TreeMap<>(); |
|
| 56 | ||
| 57 |
/** Number of compilations which reached this collector. */ |
|
| 58 |
private static int compiled = 0; |
|
| 59 | ||
| 60 |
/** Number of those which had no durable name and so could not be indexed. */ |
|
| 61 |
private static int unnamed = 0; |
|
| 62 | ||
| 63 |
/** Number of class files actually written, after folding byte identical ones together. */ |
|
| 64 |
private static int classesWritten = 0; |
|
| 65 | ||
| 66 |
/** |
|
| 67 |
* Report whether the compiler is running in generation mode. |
|
| 68 |
* |
|
| 69 |
* @return <code>true</code> while a generation run is in progress. |
|
| 70 |
*/ |
|
| 71 |
static boolean isGenerating() |
|
| 72 |
{
|
|
| 73 |
return generating; |
|
| 74 |
} |
|
| 75 | ||
| 76 |
/** |
|
| 77 |
* Turn generation mode on or off. |
|
| 78 |
* |
|
| 79 |
* @param on |
|
| 80 |
* <code>true</code> to start generating. |
|
| 81 |
*/ |
|
| 82 |
public static void setGenerating(boolean on) |
|
| 83 |
{
|
|
| 84 |
generating = on; |
|
| 85 |
} |
|
| 86 | ||
| 87 |
/** |
|
| 88 |
* Compose the class name for a compilation, in the internal, slash separated form which |
|
| 89 |
* {@link ClassFile} requires and which is embedded in the generated constant pool.
|
|
| 90 |
* |
|
| 91 |
* @param signature |
|
| 92 |
* The compilation's signature, from {@link Compiler#signature}.
|
|
| 93 |
* |
|
| 94 |
* @return Internal form of the class name. |
|
| 95 |
*/ |
|
| 96 |
static String internalName(String signature) |
|
| 97 |
{
|
|
| 98 |
return (PACKAGE + "A" + AotIndex.digest(signature)).replace('.', '/');
|
|
| 99 |
} |
|
| 100 | ||
| 101 |
/** |
|
| 102 |
* Accept a class the compiler has just assembled, together with the state a conversion needs in |
|
| 103 |
* order to bind it to a caller's scope without parsing the expression again. |
|
| 104 |
* |
|
| 105 |
* @param expression |
|
| 106 |
* Expression the class was generated for. |
|
| 107 |
* @param classFile |
|
| 108 |
* The assembled class. |
|
| 109 |
* @param returnType |
|
| 110 |
* Return type the blueprint must be configured with. |
|
| 111 |
* @param libraries |
|
| 112 |
* Library symbol to slot map. |
|
| 113 |
* @param variables |
|
| 114 |
* Variable symbol to slot map. |
|
| 115 |
*/ |
|
| 116 |
static synchronized void emit(Expression expression, ClassFile classFile, Class<?> returnType, |
|
| 117 |
Map<String, Integer> libraries, Map<String, Integer> variables) |
|
| 118 |
{
|
|
| 119 |
String aotId = expression.getAotId(); |
|
| 120 | ||
| 121 |
compiled++; |
|
| 122 | ||
| 123 |
if (aotId == null) |
|
| 124 |
{
|
|
| 125 |
// not part of a rule set, so no conversion will ever look for it |
|
| 126 |
unnamed++; |
|
| 127 | ||
| 128 |
return; |
|
| 129 |
} |
|
| 130 | ||
| 131 |
String className = classFile.getFullyQualifiedName(); |
|
| 132 | ||
| 133 |
try |
|
| 134 |
{
|
|
| 135 |
if (!classes.containsKey(className)) |
|
| 136 |
{
|
|
| 137 |
ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); |
|
| 138 |
DataOutputStream dos = new DataOutputStream(baos); |
|
| 139 | ||
| 140 |
classFile.write(dos); |
|
| 141 |
dos.flush(); |
|
| 142 | ||
| 143 |
classes.put(className, baos.toByteArray()); |
|
| 144 |
} |
|
| 145 | ||
| 146 |
index.put(aotId, |
|
| 147 |
className.substring(PACKAGE.length()) + '|' + |
|
| 148 |
expression.getRuntimeFlags() + '|' + |
|
| 149 |
AotIndex.digest(expression.getInfix()) + '|' + |
|
| 150 |
((returnType == null) ? "null" : returnType.getName()) + '|' + |
|
| 151 |
String.join(",", slotNames(libraries)) + '|' +
|
|
| 152 |
String.join(",", slotNames(variables)));
|
|
| 153 |
} |
|
| 154 |
catch (IOException exc) |
|
| 155 |
{
|
|
| 156 |
throw new IllegalStateException("could not serialize " + className, exc);
|
|
| 157 |
} |
|
| 158 |
} |
|
| 159 | ||
| 160 |
/** |
|
| 161 |
* Write everything collected so far: one class file per generated class, laid out by package so |
|
| 162 |
* that the build can fold them straight into the artifact, plus the index which maps occurrences |
|
| 163 |
* onto them. |
|
| 164 |
* |
|
| 165 |
* @param outputDir |
|
| 166 |
* Root directory to write into. |
|
| 167 |
* @param debugLevel |
|
| 168 |
* Debug level these classes were generated for. Recorded so that a conversion running |
|
| 169 |
* at a different level can say so precisely instead of just finding nothing. |
|
| 170 |
* |
|
| 171 |
* @throws IOException |
|
| 172 |
* if writing fails. |
|
| 173 |
*/ |
|
| 174 |
public static synchronized void write(File outputDir, int debugLevel) |
|
| 175 |
throws IOException |
|
| 176 |
{
|
|
| 177 |
File classDir = new File(outputDir, PACKAGE.replace('.', '/'));
|
|
| 178 | ||
| 179 |
classDir.mkdirs(); |
|
| 180 | ||
| 181 |
// Names come from the compilation signature, which is deliberately over-inclusive: it |
|
| 182 |
// separates compilations which would in fact assemble to identical bytecode. Fold those |
|
| 183 |
// together now, by fingerprinting each class with its own name masked out, so that the |
|
| 184 |
// artifact carries one copy and a conversion loads and verifies one class instead of |
|
| 185 |
// several. Class loading is the dominant cost of using these at run time, so this is worth |
|
| 186 |
// more than the disk space it saves. |
|
| 187 |
Map<String, String> canonical = new HashMap<>(classes.size()); |
|
| 188 |
Map<String, String> alias = new HashMap<>(classes.size()); |
|
| 189 | ||
| 190 |
for (Map.Entry<String, byte[]> next : classes.entrySet()) |
|
| 191 |
{
|
|
| 192 |
String name = next.getKey(); |
|
| 193 |
String print = fingerprint(next.getValue(), name.replace('.', '/'));
|
|
| 194 |
String first = canonical.putIfAbsent(print, name); |
|
| 195 | ||
| 196 |
alias.put(name, (first == null) ? name : first); |
|
| 197 |
} |
|
| 198 | ||
| 199 |
int written = 0; |
|
| 200 | ||
| 201 |
for (Map.Entry<String, byte[]> next : classes.entrySet()) |
|
| 202 |
{
|
|
| 203 |
String name = next.getKey(); |
|
| 204 | ||
| 205 |
if (!name.equals(alias.get(name))) |
|
| 206 |
{
|
|
| 207 |
// a duplicate of one already being written |
|
| 208 |
continue; |
|
| 209 |
} |
|
| 210 | ||
| 211 |
Files.write(new File(classDir, name.substring(PACKAGE.length()) + ".class").toPath(), |
|
| 212 |
next.getValue()); |
|
| 213 |
written++; |
|
| 214 |
} |
|
| 215 | ||
| 216 |
// point every occurrence at the surviving copy of its class |
|
| 217 |
for (Map.Entry<String, String> next : index.entrySet()) |
|
| 218 |
{
|
|
| 219 |
String value = next.getValue(); |
|
| 220 |
int bar = value.indexOf('|');
|
|
| 221 |
String canon = alias.get(PACKAGE + value.substring(0, bar)); |
|
| 222 | ||
| 223 |
if (canon != null) |
|
| 224 |
{
|
|
| 225 |
next.setValue(canon.substring(PACKAGE.length()) + value.substring(bar)); |
|
| 226 |
} |
|
| 227 |
} |
|
| 228 | ||
| 229 |
// One index per profile. A conversion loads profiles one at a time, so this keeps both the |
|
| 230 |
// parse cost and the resident map proportional to the profile in hand rather than to the |
|
| 231 |
// whole rule base -- which is nearly half a million occurrences. |
|
| 232 |
File indexDir = new File(classDir, "index"); |
|
| 233 | ||
| 234 |
indexDir.mkdirs(); |
|
| 235 | ||
| 236 |
Map<String, List<String>> byProfile = new TreeMap<>(); |
|
| 237 | ||
| 238 |
for (Map.Entry<String, String> next : index.entrySet()) |
|
| 239 |
{
|
|
| 240 |
String aotId = next.getKey(); |
|
| 241 |
int hash = aotId.lastIndexOf('#');
|
|
| 242 |
String profile = aotId.substring(0, hash); |
|
| 243 | ||
| 244 |
byProfile.computeIfAbsent(profile, k -> new ArrayList<>()) |
|
| 245 |
.add(aotId.substring(hash + 1) + '|' + next.getValue()); |
|
| 246 |
} |
|
| 247 | ||
| 248 |
for (Map.Entry<String, List<String>> next : byProfile.entrySet()) |
|
| 249 |
{
|
|
| 250 |
File target = new File(indexDir, AotIndex.indexFileName(next.getKey())); |
|
| 251 | ||
| 252 |
try (PrintWriter out = new PrintWriter( |
|
| 253 |
new OutputStreamWriter( |
|
| 254 |
new BufferedOutputStream(new FileOutputStream(target)), |
|
| 255 |
StandardCharsets.UTF_8))) |
|
| 256 |
{
|
|
| 257 |
out.println(AotIndex.MAGIC); |
|
| 258 | ||
| 259 |
for (String line : next.getValue()) |
|
| 260 |
{
|
|
| 261 |
out.println(line); |
|
| 262 |
} |
|
| 263 |
} |
|
| 264 |
} |
|
| 265 | ||
| 266 |
// what these classes were generated for, so a mismatch can be reported precisely |
|
| 267 |
Files.write(new File(classDir, "meta.txt").toPath(), |
|
| 268 |
("debug-level=" + debugLevel + "\n").getBytes(StandardCharsets.UTF_8));
|
|
| 269 | ||
| 270 |
classesWritten = written; |
|
| 271 |
} |
|
| 272 | ||
| 273 |
/** |
|
| 274 |
* Report how many occurrences have been indexed. |
|
| 275 |
* |
|
| 276 |
* @return Occurrence count. |
|
| 277 |
*/ |
|
| 278 |
public static synchronized int occurrences() |
|
| 279 |
{
|
|
| 280 |
return index.size(); |
|
| 281 |
} |
|
| 282 | ||
| 283 |
/** |
|
| 284 |
* Report how many distinct classes have been generated. |
|
| 285 |
* |
|
| 286 |
* @return Class count. |
|
| 287 |
*/ |
|
| 288 |
public static synchronized int classCount() |
|
| 289 |
{
|
|
| 290 |
return (classesWritten == 0) ? classes.size() : classesWritten; |
|
| 291 |
} |
|
| 292 | ||
| 293 |
/** |
|
| 294 |
* Fingerprint class file bytes, ignoring every occurrence of the class's own internal name. |
|
| 295 |
* <p> |
|
| 296 |
* That name is the one part which differs between two otherwise identical classes, and it is a |
|
| 297 |
* constant pool UTF8 entry preceded by its own two byte length, so both are dropped. |
|
| 298 |
* |
|
| 299 |
* @param raw |
|
| 300 |
* Class file bytes. |
|
| 301 |
* @param internalName |
|
| 302 |
* The class's own name in internal form. |
|
| 303 |
* |
|
| 304 |
* @return Hex encoded digest of the name independent content. |
|
| 305 |
*/ |
|
| 306 |
private static String fingerprint(byte[] raw, String internalName) |
|
| 307 |
{
|
|
| 308 |
try |
|
| 309 |
{
|
|
| 310 |
byte[] needle = internalName.getBytes(StandardCharsets.UTF_8); |
|
| 311 |
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
| 312 |
int pos = 0; |
|
| 313 | ||
| 314 |
for (int i = 0; i <= raw.length - needle.length; ) |
|
| 315 |
{
|
|
| 316 |
if (matches(raw, i, needle)) |
|
| 317 |
{
|
|
| 318 |
digest.update(raw, pos, Math.max(pos, i - 2) - pos); |
|
| 319 | ||
| 320 |
i += needle.length; |
|
| 321 |
pos = i; |
|
| 322 |
} |
|
| 323 |
else |
|
| 324 |
{
|
|
| 325 |
i++; |
|
| 326 |
} |
|
| 327 |
} |
|
| 328 | ||
| 329 |
digest.update(raw, pos, raw.length - pos); |
|
| 330 | ||
| 331 |
StringBuilder buf = new StringBuilder(64); |
|
| 332 | ||
| 333 |
for (byte b : digest.digest()) |
|
| 334 |
{
|
|
| 335 |
buf.append(Character.forDigit((b >> 4) & 0xf, 16)); |
|
| 336 |
buf.append(Character.forDigit(b & 0xf, 16)); |
|
| 337 |
} |
|
| 338 | ||
| 339 |
return buf.toString(); |
|
| 340 |
} |
|
| 341 |
catch (NoSuchAlgorithmException exc) |
|
| 342 |
{
|
|
| 343 |
throw new IllegalStateException(exc); |
|
| 344 |
} |
|
| 345 |
} |
|
| 346 | ||
| 347 |
/** |
|
| 348 |
* Test whether <code>needle</code> occurs in <code>raw</code> at <code>at</code>. |
|
| 349 |
* |
|
| 350 |
* @param raw |
|
| 351 |
* Buffer to search. |
|
| 352 |
* @param at |
|
| 353 |
* Offset at which to compare. |
|
| 354 |
* @param needle |
|
| 355 |
* Bytes to look for. |
|
| 356 |
* |
|
| 357 |
* @return <code>true</code> on a match. |
|
| 358 |
*/ |
|
| 359 |
private static boolean matches(byte[] raw, int at, byte[] needle) |
|
| 360 |
{
|
|
| 361 |
for (int i = 0; i < needle.length; i++) |
|
| 362 |
{
|
|
| 363 |
if (raw[at + i] != needle[i]) |
|
| 364 |
{
|
|
| 365 |
return false; |
|
| 366 |
} |
|
| 367 |
} |
|
| 368 | ||
| 369 |
return true; |
|
| 370 |
} |
|
| 371 | ||
| 372 |
/** |
|
| 373 |
* Render a symbol to slot map as a list ordered by slot. |
|
| 374 |
* |
|
| 375 |
* @param slots |
|
| 376 |
* Map of symbol name to slot index. |
|
| 377 |
* |
|
| 378 |
* @return Symbol names in slot order. |
|
| 379 |
*/ |
|
| 380 |
private static List<String> slotNames(Map<String, Integer> slots) |
|
| 381 |
{
|
|
| 382 |
String[] byIndex = new String[slots.size()]; |
|
| 383 | ||
| 384 |
for (Map.Entry<String, Integer> next : slots.entrySet()) |
|
| 385 |
{
|
|
| 386 |
byIndex[next.getValue()] = next.getKey(); |
|
| 387 |
} |
|
| 388 | ||
| 389 |
return Arrays.asList(byIndex); |
|
| 390 |
} |
|
| 391 | ||
| 392 |
/** |
|
| 393 |
* Not instantiable. |
|
| 394 |
*/ |
|
| 395 |
private AotGenerator() |
|
| 396 |
{
|
|
| 397 |
} |
|
| 398 |
} |
|
| new/src/com/goldencode/expr/AotIndex.java 2026-09-25 09:27:23.846124763 +0300 | ||
|---|---|---|
| 1 |
/* |
|
| 2 |
** Module : AotIndex.java |
|
| 3 |
** Abstract : Index of expression classes generated when FWD itself was built |
|
| 4 |
** |
|
| 5 |
** Copyright (c) 2026, Golden Code Development Corporation. |
|
| 6 |
** |
|
| 7 |
** -#- -I- --Date-- ----------------Description----------------- |
|
| 8 |
** 001 TG 20260909 Initial version. |
|
| 9 |
*/ |
|
| 10 | ||
| 11 |
package com.goldencode.expr; |
|
| 12 | ||
| 13 |
import java.io.*; |
|
| 14 |
import java.nio.charset.*; |
|
| 15 |
import java.security.*; |
|
| 16 |
import java.util.*; |
|
| 17 | ||
| 18 |
/** |
|
| 19 |
* Index of the expression classes which were generated when FWD was built, and which ship inside |
|
| 20 |
* the same artifact as the rule sets they came from. |
|
| 21 |
* |
|
| 22 |
* <h3>What this replaces</h3> |
|
| 23 |
* |
|
| 24 |
* Left to itself a conversion generates, defines and JIT compiles a class for every expression of |
|
| 25 |
* every rule set it loads, on every run. Those rule sets are fixed content shipped with FWD, so |
|
| 26 |
* the work is the same every time and can be done once, at build time. A run which finds an |
|
| 27 |
* occurrence here loads an ordinary class from the classpath instead. |
|
| 28 |
* |
|
| 29 |
* <h3>How an occurrence is named</h3> |
|
| 30 |
* |
|
| 31 |
* Every expression belonging to a rule set is created during a profile load, which reads a fixed |
|
| 32 |
* set of files in document order on a single thread, so each occurrence has a stable position in |
|
| 33 |
* that sequence. {@link Expression#assignAotId} turns that into a durable name, and this index is
|
|
| 34 |
* keyed on it. Because the index and the rule sets are produced by one build and shipped in one |
|
| 35 |
* artifact, they cannot disagree about that sequence. |
|
| 36 |
* |
|
| 37 |
* <h3>What an entry holds beyond the class</h3> |
|
| 38 |
* |
|
| 39 |
* The class alone is not enough: {@link Compiler#instantiate} binds a compiled expression to the
|
|
| 40 |
* libraries and variables of the calling scope through the symbol-to-slot maps that parsing |
|
| 41 |
* produced, and the blueprint needs its return type. Recording those is what removes parsing from |
|
| 42 |
* the fast path. |
|
| 43 |
* |
|
| 44 |
* <h3>Failing safe</h3> |
|
| 45 |
* |
|
| 46 |
* Every entry carries a digest of the expression text it was generated from, checked before the |
|
| 47 |
* class is used. In a released build that check can never fail; it exists for the developer case |
|
| 48 |
* where rules have been edited without regenerating, and it turns what would be a wrong class into |
|
| 49 |
* an ordinary compilation. A missing index, an unreadable entry, an absent class -- all degrade to |
|
| 50 |
* compiling, never to misbehaving. |
|
| 51 |
*/ |
|
| 52 |
final class AotIndex |
|
| 53 |
{
|
|
| 54 |
/** Package holding every generated class. */ |
|
| 55 |
static final String PACKAGE = "com.goldencode.expr.aot."; |
|
| 56 | ||
| 57 |
/** Resource directory holding the per profile indexes, relative to the classpath root. */ |
|
| 58 |
static final String RESOURCE_DIR = "com/goldencode/expr/aot/index/"; |
|
| 59 | ||
| 60 |
/** Identity of the inputs the generated classes were built from, inside fwd-trpl.jar. */ |
|
| 61 |
static final String STAMP_RESOURCE = "com/goldencode/expr/aot/stamp.txt"; |
|
| 62 | ||
| 63 |
/** Identity those classes are expected to have, inside p2j.jar. */ |
|
| 64 |
static final String EXPECTED_RESOURCE = "com/goldencode/expr/aot-expected.txt"; |
|
| 65 | ||
| 66 |
/** What the generated classes were built for, inside fwd-trpl.jar. */ |
|
| 67 |
static final String META_RESOURCE = "com/goldencode/expr/aot/meta.txt"; |
|
| 68 | ||
| 69 |
/** Format marker of the index. */ |
|
| 70 |
static final String MAGIC = "#fwd-aot 2"; |
|
| 71 | ||
| 72 |
/** Occurrences served from the index. */ |
|
| 73 |
private static int hits = 0; |
|
| 74 | ||
| 75 |
/** Occurrences which had to be compiled. */ |
|
| 76 |
private static int misses = 0; |
|
| 77 | ||
| 78 |
/** Occurrences compiled because they belong to no profile -- expression text built at run time. */ |
|
| 79 |
private static int unnamedMisses = 0; |
|
| 80 | ||
| 81 |
/** Hits and misses per profile, so a shortfall can be attributed rather than guessed at. */ |
|
| 82 |
private static final Map<String, int[]> byProfile = new TreeMap<>(); |
|
| 83 | ||
| 84 |
/** Entries of the profile currently loaded, keyed by occurrence ordinal. */ |
|
| 85 |
private static volatile Map<Integer, Entry> entries = Collections.emptyMap(); |
|
| 86 | ||
| 87 |
/** Profile whose entries are resident, or <code>null</code>. */ |
|
| 88 |
private static volatile String resident = null; |
|
| 89 | ||
| 90 |
/** |
|
| 91 |
* Flag disabling use of the generated classes, so a conversion compiles expressions exactly as |
|
| 92 |
* it would without them. Set <code>P2J_AOT_DISABLE</code> in the environment. Kept as an |
|
| 93 |
* escape hatch: if a generated class is ever suspected, this turns the mechanism off without |
|
| 94 |
* rebuilding or repackaging anything. |
|
| 95 |
*/ |
|
| 96 |
private static final boolean DISABLED = (System.getenv("P2J_AOT_DISABLE") != null);
|
|
| 97 | ||
| 98 |
/** |
|
| 99 |
* The artifact holding the generated classes -- fwd-trpl.jar -- or <code>null</code> when it is |
|
| 100 |
* absent, unreadable, or does not match this build. |
|
| 101 |
* <p> |
|
| 102 |
* It is located once, by asking the classpath for its stamp, and thereafter read directly. The |
|
| 103 |
* conversion classpath is several hundred jars, so searching it per class is what this avoids. |
|
| 104 |
*/ |
|
| 105 |
private static final Object SOURCE = openSource(); |
|
| 106 | ||
| 107 |
/** Loader which resolves the generated classes straight out of {@link #SOURCE}. */
|
|
| 108 |
private static final AotClassLoader LOADER = new AotClassLoader(AotIndex.class.getClassLoader()); |
|
| 109 | ||
| 110 |
/** |
|
| 111 |
* Not instantiable. |
|
| 112 |
*/ |
|
| 113 |
private AotIndex() |
|
| 114 |
{
|
|
| 115 |
} |
|
| 116 | ||
| 117 |
/** |
|
| 118 |
* Compose the resource name holding a profile's index. Profile names contain path separators, |
|
| 119 |
* which are folded so that every index is one flat resource. |
|
| 120 |
* |
|
| 121 |
* @param profile |
|
| 122 |
* Profile name. |
|
| 123 |
* |
|
| 124 |
* @return Resource file name. |
|
| 125 |
*/ |
|
| 126 |
public static String indexFileName(String profile) |
|
| 127 |
{
|
|
| 128 |
return profile.replace('/', '.') + ".txt";
|
|
| 129 |
} |
|
| 130 | ||
| 131 |
static |
|
| 132 |
{
|
|
| 133 |
Runtime.getRuntime().addShutdownHook(new Thread(AotIndex::report)); |
|
| 134 |
} |
|
| 135 | ||
| 136 |
/** |
|
| 137 |
* Describe what the generated classes were built for. |
|
| 138 |
* |
|
| 139 |
* @return A short description, for reporting. |
|
| 140 |
*/ |
|
| 141 |
private static String describeGeneratedFor() |
|
| 142 |
{
|
|
| 143 |
String meta = text(bytesOf(META_RESOURCE)); |
|
| 144 | ||
| 145 |
return (meta == null) ? "an unrecorded debug level" : meta.replace('=', ' ');
|
|
| 146 |
} |
|
| 147 | ||
| 148 |
/** |
|
| 149 |
* Report whether any entry is currently resident. |
|
| 150 |
* |
|
| 151 |
* @return <code>true</code> if a lookup could succeed. |
|
| 152 |
*/ |
|
| 153 |
static boolean isAvailable() |
|
| 154 |
{
|
|
| 155 |
return !entries.isEmpty(); |
|
| 156 |
} |
|
| 157 | ||
| 158 |
/** |
|
| 159 |
* Make the generated entries of a profile resident, replacing whichever profile was resident |
|
| 160 |
* before. Called as a profile's rule sets begin loading. |
|
| 161 |
* |
|
| 162 |
* @param profile |
|
| 163 |
* Profile being loaded, or <code>null</code> to drop what is resident. |
|
| 164 |
*/ |
|
| 165 |
static synchronized void selectProfile(String profile) |
|
| 166 |
{
|
|
| 167 |
if (DISABLED || profile == null || profile.equals(resident)) |
|
| 168 |
{
|
|
| 169 |
return; |
|
| 170 |
} |
|
| 171 | ||
| 172 |
resident = profile; |
|
| 173 |
entries = read(profile); |
|
| 174 |
} |
|
| 175 | ||
| 176 |
/** |
|
| 177 |
* Load one of the generated classes. |
|
| 178 |
* |
|
| 179 |
* @param className |
|
| 180 |
* Fully qualified name of the class. |
|
| 181 |
* |
|
| 182 |
* @return The class. |
|
| 183 |
* |
|
| 184 |
* @throws ClassNotFoundException |
|
| 185 |
* if it is not present. |
|
| 186 |
*/ |
|
| 187 |
static Class<?> loadClass(String className) |
|
| 188 |
throws ClassNotFoundException |
|
| 189 |
{
|
|
| 190 |
return LOADER.loadClass(className, true); |
|
| 191 |
} |
|
| 192 | ||
| 193 |
/** |
|
| 194 |
* Note that an occurrence was served from the index. |
|
| 195 |
*/ |
|
| 196 |
static synchronized void hit() |
|
| 197 |
{
|
|
| 198 |
hits++; |
|
| 199 |
tally(resident, 0); |
|
| 200 |
} |
|
| 201 | ||
| 202 |
/** |
|
| 203 |
* Note that an occurrence had to be compiled. |
|
| 204 |
*/ |
|
| 205 |
static synchronized void miss() |
|
| 206 |
{
|
|
| 207 |
misses++; |
|
| 208 |
tally(resident, 1); |
|
| 209 |
} |
|
| 210 | ||
| 211 |
/** |
|
| 212 |
* Note that an occurrence had to be compiled because it has no durable name at all, which means |
|
| 213 |
* its text was assembled at run time rather than read from a rule set. Nothing can be |
|
| 214 |
* generated for those. |
|
| 215 |
*/ |
|
| 216 |
static synchronized void unnamedMiss() |
|
| 217 |
{
|
|
| 218 |
misses++; |
|
| 219 |
unnamedMisses++; |
|
| 220 |
} |
|
| 221 | ||
| 222 |
/** |
|
| 223 |
* Record an outcome against the profile currently loaded. |
|
| 224 |
* |
|
| 225 |
* @param profile |
|
| 226 |
* Profile in force, or <code>null</code>. |
|
| 227 |
* @param slot |
|
| 228 |
* 0 for a hit, 1 for a miss. |
|
| 229 |
*/ |
|
| 230 |
private static void tally(String profile, int slot) |
|
| 231 |
{
|
|
| 232 |
byProfile.computeIfAbsent((profile == null) ? "<none>" : profile, k -> new int[2])[slot]++; |
|
| 233 |
} |
|
| 234 | ||
| 235 |
/** |
|
| 236 |
* Look up the class generated for an expression occurrence. |
|
| 237 |
* |
|
| 238 |
* @param aotId |
|
| 239 |
* Durable name of the occurrence, or <code>null</code> if it has none. |
|
| 240 |
* @param infix |
|
| 241 |
* The occurrence's expression text, checked against the text the entry was generated |
|
| 242 |
* from. |
|
| 243 |
* @param flags |
|
| 244 |
* The occurrence's runtime flags, likewise checked. |
|
| 245 |
* |
|
| 246 |
* @return The entry, or <code>null</code> if there is none or it does not describe this |
|
| 247 |
* expression, in which case the caller compiles as usual. |
|
| 248 |
*/ |
|
| 249 |
static Entry lookup(String aotId, String infix, int flags) |
|
| 250 |
{
|
|
| 251 |
if (aotId == null || entries.isEmpty()) |
|
| 252 |
{
|
|
| 253 |
return null; |
|
| 254 |
} |
|
| 255 | ||
| 256 |
int hash = aotId.lastIndexOf('#');
|
|
| 257 | ||
| 258 |
if (hash < 0 || !aotId.regionMatches(0, resident, 0, hash)) |
|
| 259 |
{
|
|
| 260 |
// an occurrence of some other profile; only one profile's entries are resident |
|
| 261 |
return null; |
|
| 262 |
} |
|
| 263 | ||
| 264 |
Entry entry = entries.get(Integer.valueOf(aotId.substring(hash + 1))); |
|
| 265 | ||
| 266 |
if (entry == null || entry.flags != flags || !entry.infixDigest.equals(digest(infix))) |
|
| 267 |
{
|
|
| 268 |
return null; |
|
| 269 |
} |
|
| 270 | ||
| 271 |
return entry; |
|
| 272 |
} |
|
| 273 | ||
| 274 |
/** |
|
| 275 |
* Digest an expression text. |
|
| 276 |
* |
|
| 277 |
* @param text |
|
| 278 |
* Text to digest. |
|
| 279 |
* |
|
| 280 |
* @return Truncated hex encoded digest. |
|
| 281 |
*/ |
|
| 282 |
static String digest(String text) |
|
| 283 |
{
|
|
| 284 |
try |
|
| 285 |
{
|
|
| 286 |
byte[] raw = MessageDigest.getInstance("SHA-256")
|
|
| 287 |
.digest(text.getBytes(StandardCharsets.UTF_8)); |
|
| 288 |
StringBuilder buf = new StringBuilder(32); |
|
| 289 | ||
| 290 |
for (byte b : raw) |
|
| 291 |
{
|
|
| 292 |
buf.append(Character.forDigit((b >> 4) & 0xf, 16)); |
|
| 293 |
buf.append(Character.forDigit(b & 0xf, 16)); |
|
| 294 |
} |
|
| 295 | ||
| 296 |
return buf.substring(0, 24); |
|
| 297 |
} |
|
| 298 |
catch (NoSuchAlgorithmException exc) |
|
| 299 |
{
|
|
| 300 |
throw new IllegalStateException(exc); |
|
| 301 |
} |
|
| 302 |
} |
|
| 303 | ||
| 304 |
/** |
|
| 305 |
* Read the index from the classpath. |
|
| 306 |
* |
|
| 307 |
* @return The entries, or an empty map if no index is present or it cannot be parsed. |
|
| 308 |
*/ |
|
| 309 |
private static Map<Integer, Entry> read(String profile) |
|
| 310 |
{
|
|
| 311 |
byte[] raw = bytesOf(RESOURCE_DIR + indexFileName(profile)); |
|
| 312 | ||
| 313 |
if (raw == null) |
|
| 314 |
{
|
|
| 315 |
// FWD built without generated expressions for this profile; it compiles as it always did |
|
| 316 |
return Collections.emptyMap(); |
|
| 317 |
} |
|
| 318 | ||
| 319 |
Map<Integer, Entry> loaded = new HashMap<>(16384); |
|
| 320 | ||
| 321 |
try (BufferedReader reader = new BufferedReader( |
|
| 322 |
new InputStreamReader(new ByteArrayInputStream(raw), StandardCharsets.UTF_8))) |
|
| 323 |
{
|
|
| 324 |
String header = reader.readLine(); |
|
| 325 | ||
| 326 |
if (header == null || !header.startsWith(MAGIC)) |
|
| 327 |
{
|
|
| 328 |
System.err.println("AotIndex: unrecognized index format, ignoring");
|
|
| 329 | ||
| 330 |
return Collections.emptyMap(); |
|
| 331 |
} |
|
| 332 | ||
| 333 |
String line; |
|
| 334 | ||
| 335 |
while ((line = reader.readLine()) != null) |
|
| 336 |
{
|
|
| 337 |
if (line.isEmpty() || line.charAt(0) == '#') |
|
| 338 |
{
|
|
| 339 |
continue; |
|
| 340 |
} |
|
| 341 | ||
| 342 |
String[] f = line.split("\\|", 7);
|
|
| 343 | ||
| 344 |
if (f.length == 7) |
|
| 345 |
{
|
|
| 346 |
loaded.put(Integer.valueOf(f[0]), |
|
| 347 |
new Entry(PACKAGE + f[1], Integer.parseInt(f[2]), f[3], f[4], |
|
| 348 |
split(f[5]), split(f[6]))); |
|
| 349 |
} |
|
| 350 |
} |
|
| 351 |
} |
|
| 352 |
catch (Exception exc) |
|
| 353 |
{
|
|
| 354 |
System.err.println("AotIndex: could not read index, ignoring: " + exc);
|
|
| 355 | ||
| 356 |
return Collections.emptyMap(); |
|
| 357 |
} |
|
| 358 | ||
| 359 |
return loaded; |
|
| 360 |
} |
|
| 361 | ||
| 362 |
/** |
|
| 363 |
* Split a comma separated symbol list. |
|
| 364 |
* |
|
| 365 |
* @param text |
|
| 366 |
* Encoded list, possibly empty. |
|
| 367 |
* |
|
| 368 |
* @return The symbol names, in slot order. |
|
| 369 |
*/ |
|
| 370 |
private static List<String> split(String text) |
|
| 371 |
{
|
|
| 372 |
return text.isEmpty() ? Collections.emptyList() : Arrays.asList(text.split(","));
|
|
| 373 |
} |
|
| 374 | ||
| 375 |
/** |
|
| 376 |
* Locate fwd-trpl.jar, but only if it was generated from the same inputs as this p2j.jar. |
|
| 377 |
* <p> |
|
| 378 |
* The two carry a shared stamp over the rule sets and the expression compiler. Disagreement |
|
| 379 |
* means the generated classes describe rules or code generation which no longer exist, so they |
|
| 380 |
* are ignored outright and every expression is compiled as it always was. That is the whole |
|
| 381 |
* staleness story: the artifact is optional and regenerable, and being wrong is never a |
|
| 382 |
* possible outcome of it being old. |
|
| 383 |
* |
|
| 384 |
* @return An open {@link java.util.jar.JarFile}, a {@link File} directory, or
|
|
| 385 |
* <code>null</code> if the generated classes should not be used. |
|
| 386 |
*/ |
|
| 387 |
private static Object openSource() |
|
| 388 |
{
|
|
| 389 |
try |
|
| 390 |
{
|
|
| 391 |
ClassLoader loader = AotIndex.class.getClassLoader(); |
|
| 392 |
String expected = text(loader.getResourceAsStream(EXPECTED_RESOURCE)); |
|
| 393 | ||
| 394 |
if (expected == null) |
|
| 395 |
{
|
|
| 396 |
// this FWD build ships no generated classes |
|
| 397 |
return null; |
|
| 398 |
} |
|
| 399 | ||
| 400 |
java.net.URL url = loader.getResource(STAMP_RESOURCE); |
|
| 401 | ||
| 402 |
if (url == null) |
|
| 403 |
{
|
|
| 404 |
// fwd-trpl.jar is not on the classpath; perfectly fine, just slower |
|
| 405 |
return null; |
|
| 406 |
} |
|
| 407 | ||
| 408 |
String actual = text(url.openStream()); |
|
| 409 | ||
| 410 |
if (!expected.equals(actual)) |
|
| 411 |
{
|
|
| 412 |
System.err.println("AotIndex: fwd-trpl.jar was generated from different sources than " +
|
|
| 413 |
"this p2j.jar (expected " + expected + ", found " + actual + |
|
| 414 |
"); ignoring it and compiling expressions normally"); |
|
| 415 | ||
| 416 |
return null; |
|
| 417 |
} |
|
| 418 | ||
| 419 |
if ("jar".equals(url.getProtocol()))
|
|
| 420 |
{
|
|
| 421 |
java.net.JarURLConnection conn = (java.net.JarURLConnection) url.openConnection(); |
|
| 422 | ||
| 423 |
conn.setUseCaches(false); |
|
| 424 | ||
| 425 |
return new java.util.jar.JarFile(new File(conn.getJarFileURL().toURI())); |
|
| 426 |
} |
|
| 427 | ||
| 428 |
if ("file".equals(url.getProtocol()))
|
|
| 429 |
{
|
|
| 430 |
// exploded on disk: climb back out of the resource path to the classpath root |
|
| 431 |
File root = new File(url.toURI()); |
|
| 432 | ||
| 433 |
for (int i = 0; i < STAMP_RESOURCE.split("/").length; i++)
|
|
| 434 |
{
|
|
| 435 |
root = root.getParentFile(); |
|
| 436 |
} |
|
| 437 | ||
| 438 |
return root; |
|
| 439 |
} |
|
| 440 | ||
| 441 |
return null; |
|
| 442 |
} |
|
| 443 |
catch (Exception exc) |
|
| 444 |
{
|
|
| 445 |
System.err.println("AotIndex: could not open fwd-trpl.jar, compiling normally: " + exc);
|
|
| 446 | ||
| 447 |
return null; |
|
| 448 |
} |
|
| 449 |
} |
|
| 450 | ||
| 451 |
/** |
|
| 452 |
* Read a small text resource, trimmed. |
|
| 453 |
* |
|
| 454 |
* @param in |
|
| 455 |
* Stream to read, possibly <code>null</code>. Closed on return. |
|
| 456 |
* |
|
| 457 |
* @return The trimmed content, or <code>null</code>. |
|
| 458 |
*/ |
|
| 459 |
private static String text(InputStream in) |
|
| 460 |
{
|
|
| 461 |
if (in == null) |
|
| 462 |
{
|
|
| 463 |
return null; |
|
| 464 |
} |
|
| 465 | ||
| 466 |
try (InputStream stream = in) |
|
| 467 |
{
|
|
| 468 |
return new String(stream.readAllBytes(), StandardCharsets.UTF_8).trim(); |
|
| 469 |
} |
|
| 470 |
catch (IOException exc) |
|
| 471 |
{
|
|
| 472 |
return null; |
|
| 473 |
} |
|
| 474 |
} |
|
| 475 | ||
| 476 |
/** |
|
| 477 |
* Interpret bytes as trimmed text. |
|
| 478 |
* |
|
| 479 |
* @param raw |
|
| 480 |
* Bytes, possibly <code>null</code>. |
|
| 481 |
* |
|
| 482 |
* @return The trimmed content, or <code>null</code>. |
|
| 483 |
*/ |
|
| 484 |
private static String text(byte[] raw) |
|
| 485 |
{
|
|
| 486 |
return (raw == null) ? null : new String(raw, StandardCharsets.UTF_8).trim(); |
|
| 487 |
} |
|
| 488 | ||
| 489 |
/** |
|
| 490 |
* Read one entry from the artifact. |
|
| 491 |
* |
|
| 492 |
* @param path |
|
| 493 |
* Entry path, relative to the artifact root. |
|
| 494 |
* |
|
| 495 |
* @return The bytes, or <code>null</code> if absent. |
|
| 496 |
*/ |
|
| 497 |
private static byte[] bytesOf(String path) |
|
| 498 |
{
|
|
| 499 |
try |
|
| 500 |
{
|
|
| 501 |
if (SOURCE instanceof java.util.jar.JarFile) |
|
| 502 |
{
|
|
| 503 |
java.util.jar.JarFile jar = (java.util.jar.JarFile) SOURCE; |
|
| 504 |
java.util.zip.ZipEntry entry = jar.getEntry(path); |
|
| 505 | ||
| 506 |
if (entry == null) |
|
| 507 |
{
|
|
| 508 |
return null; |
|
| 509 |
} |
|
| 510 | ||
| 511 |
try (InputStream in = jar.getInputStream(entry)) |
|
| 512 |
{
|
|
| 513 |
return in.readAllBytes(); |
|
| 514 |
} |
|
| 515 |
} |
|
| 516 | ||
| 517 |
if (SOURCE instanceof File) |
|
| 518 |
{
|
|
| 519 |
File file = new File((File) SOURCE, path); |
|
| 520 | ||
| 521 |
return file.isFile() ? java.nio.file.Files.readAllBytes(file.toPath()) : null; |
|
| 522 |
} |
|
| 523 | ||
| 524 |
// no usable artifact: nothing to read, and the caller compiles instead |
|
| 525 |
return null; |
|
| 526 |
} |
|
| 527 |
catch (IOException exc) |
|
| 528 |
{
|
|
| 529 |
return null; |
|
| 530 |
} |
|
| 531 |
} |
|
| 532 | ||
| 533 |
/** |
|
| 534 |
* Report what the generated classes did for this run. A run which loaded an index but matched |
|
| 535 |
* nothing is called out: that is what a conversion looks like when its debug level differs |
|
| 536 |
* from the one the classes were generated for, and it would otherwise be invisible. |
|
| 537 |
*/ |
|
| 538 |
private static void report() |
|
| 539 |
{
|
|
| 540 |
if (DISABLED) |
|
| 541 |
{
|
|
| 542 |
return; |
|
| 543 |
} |
|
| 544 | ||
| 545 |
System.err.println("AotIndex: served " + hits + ", compiled " + misses +
|
|
| 546 |
((unnamedMisses == 0) ? "" |
|
| 547 |
: (" (" + unnamedMisses +
|
|
| 548 |
" of them expressions built at run time, which " + |
|
| 549 |
"cannot be generated ahead of time)"))); |
|
| 550 | ||
| 551 |
// Attribute the shortfall. A profile which misses wholesale was not generated -- excluded |
|
| 552 |
// from the build, or it failed to load in the generator -- and that is worth knowing, |
|
| 553 |
// because it is fixable in a way that individual misses are not. |
|
| 554 |
byProfile.entrySet() |
|
| 555 |
.stream() |
|
| 556 |
.filter(e -> e.getValue()[1] > 0) |
|
| 557 |
.sorted((a, b) -> b.getValue()[1] - a.getValue()[1]) |
|
| 558 |
.limit(10) |
|
| 559 |
.forEach(e -> System.err.println("AotIndex: " + e.getKey() + " served " +
|
|
| 560 |
e.getValue()[0] + ", compiled " + |
|
| 561 |
e.getValue()[1])); |
|
| 562 | ||
| 563 |
if (hits == 0 && misses > 0) |
|
| 564 |
{
|
|
| 565 |
System.err.println("AotIndex: WARNING - an index was found but no occurrence matched it. " +
|
|
| 566 |
"The usual cause is a conversion debug level (the Dn option) different " + |
|
| 567 |
"from the one the classes were generated for; expressions are being " + |
|
| 568 |
"compiled as if the generated classes were absent."); |
|
| 569 |
} |
|
| 570 |
} |
|
| 571 | ||
| 572 |
/** |
|
| 573 |
* One indexed occurrence: which class serves it, and the state needed to bind that class to a |
|
| 574 |
* caller's scope without parsing the expression again. |
|
| 575 |
*/ |
|
| 576 |
static final class Entry |
|
| 577 |
{
|
|
| 578 |
/** Fully qualified name of the class which serves this occurrence. */ |
|
| 579 |
final String className; |
|
| 580 | ||
| 581 |
/** Runtime flags the class was generated under. */ |
|
| 582 |
final int flags; |
|
| 583 | ||
| 584 |
/** Digest of the expression text the class was generated from. */ |
|
| 585 |
final String infixDigest; |
|
| 586 | ||
| 587 |
/** Name of the blueprint's return type. */ |
|
| 588 |
final String returnType; |
|
| 589 | ||
| 590 |
/** Library symbol names, in slot order. */ |
|
| 591 |
final List<String> libraries; |
|
| 592 | ||
| 593 |
/** Variable symbol names, in slot order. */ |
|
| 594 |
final List<String> variables; |
|
| 595 | ||
| 596 |
/** |
|
| 597 |
* Constructor. |
|
| 598 |
* |
|
| 599 |
* @param className |
|
| 600 |
* Fully qualified name of the class which serves this occurrence. |
|
| 601 |
* @param flags |
|
| 602 |
* Runtime flags the class was generated under. |
|
| 603 |
* @param infixDigest |
|
| 604 |
* Digest of the expression text the class was generated from. |
|
| 605 |
* @param returnType |
|
| 606 |
* Name of the blueprint's return type. |
|
| 607 |
* @param libraries |
|
| 608 |
* Library symbol names, in slot order. |
|
| 609 |
* @param variables |
|
| 610 |
* Variable symbol names, in slot order. |
|
| 611 |
*/ |
|
| 612 |
Entry(String className, int flags, String infixDigest, String returnType, |
|
| 613 |
List<String> libraries, List<String> variables) |
|
| 614 |
{
|
|
| 615 |
this.className = className; |
|
| 616 |
this.flags = flags; |
|
| 617 |
this.infixDigest = infixDigest; |
|
| 618 |
this.returnType = returnType; |
|
| 619 |
this.libraries = libraries; |
|
| 620 |
this.variables = variables; |
|
| 621 |
} |
|
| 622 |
} |
|
| 623 | ||
| 624 |
/** |
|
| 625 |
* Loader for the generated expression classes. |
|
| 626 |
* |
|
| 627 |
* <h3>Why not the application loader</h3> |
|
| 628 |
* |
|
| 629 |
* The conversion classpath is several hundred jars. Resolving a generated class through the |
|
| 630 |
* application loader searches all of them, and doing that once per generated class costs |
|
| 631 |
* seconds. This loader knows the single artifact they live in, so a load is one zip entry read. |
|
| 632 |
* |
|
| 633 |
* <h3>Why loadClass is overridden and not just findClass</h3> |
|
| 634 |
* |
|
| 635 |
* The inherited <code>loadClass</code> is parent first: it would consult the application loader |
|
| 636 |
* -- and therefore search that whole classpath -- before ever calling {@link #findClass}, which
|
|
| 637 |
* is exactly the cost this loader exists to avoid. Generated classes are never on the parent's |
|
| 638 |
* classpath, so for them the order is inverted. Everything else, including the superclass of |
|
| 639 |
* every generated class and every type its code refers to, delegates normally. |
|
| 640 |
*/ |
|
| 641 |
private static final class AotClassLoader extends ClassLoader |
|
| 642 |
{
|
|
| 643 |
static |
|
| 644 |
{
|
|
| 645 |
// per class name locking rather than one lock for the loader: conversion threads load |
|
| 646 |
// these concurrently |
|
| 647 |
registerAsParallelCapable(); |
|
| 648 |
} |
|
| 649 | ||
| 650 |
/** |
|
| 651 |
* Constructor. |
|
| 652 |
* |
|
| 653 |
* @param parent |
|
| 654 |
* Loader to delegate everything but the generated classes to. |
|
| 655 |
*/ |
|
| 656 |
AotClassLoader(ClassLoader parent) |
|
| 657 |
{
|
|
| 658 |
super(parent); |
|
| 659 |
} |
|
| 660 | ||
| 661 |
/** |
|
| 662 |
* {@inheritDoc}
|
|
| 663 |
*/ |
|
| 664 |
@Override |
|
| 665 |
protected Class<?> loadClass(String name, boolean resolve) |
|
| 666 |
throws ClassNotFoundException |
|
| 667 |
{
|
|
| 668 |
if (!name.startsWith(PACKAGE)) |
|
| 669 |
{
|
|
| 670 |
return super.loadClass(name, resolve); |
|
| 671 |
} |
|
| 672 | ||
| 673 |
synchronized (getClassLoadingLock(name)) |
|
| 674 |
{
|
|
| 675 |
Class<?> found = findLoadedClass(name); |
|
| 676 | ||
| 677 |
if (found == null) |
|
| 678 |
{
|
|
| 679 |
found = findClass(name); |
|
| 680 |
} |
|
| 681 | ||
| 682 |
if (resolve) |
|
| 683 |
{
|
|
| 684 |
resolveClass(found); |
|
| 685 |
} |
|
| 686 | ||
| 687 |
return found; |
|
| 688 |
} |
|
| 689 |
} |
|
| 690 | ||
| 691 |
/** |
|
| 692 |
* {@inheritDoc}
|
|
| 693 |
*/ |
|
| 694 |
@Override |
|
| 695 |
protected Class<?> findClass(String name) |
|
| 696 |
throws ClassNotFoundException |
|
| 697 |
{
|
|
| 698 |
byte[] data = bytesOf(name.replace('.', '/') + ".class");
|
|
| 699 | ||
| 700 |
if (data == null) |
|
| 701 |
{
|
|
| 702 |
throw new ClassNotFoundException(name); |
|
| 703 |
} |
|
| 704 | ||
| 705 |
return defineClass(name, data, 0, data.length); |
|
| 706 |
} |
|
| 707 |
} |
|
| 708 |
} |
|
| new/src/com/goldencode/expr/Compiler.java 2026-09-25 09:27:23.846480642 +0300 | ||
|---|---|---|
| 57 | 57 |
** Cloned global variables during instantiation. |
| 58 | 58 |
** Reserved class name atomically upfront in process() and synchronize |
| 59 | 59 |
** load() on the shared loader instance to fix concurrent compilation. |
| 60 |
** 019 TG 20260909 compile() takes the compile-time scope, which is now part of the |
|
| 61 |
** compiler cache key. |
|
| 60 | 62 |
*/ |
| 61 | 63 | |
| 62 | 64 |
/* |
| ... | ... | |
| 117 | 119 |
import java.io.*; |
| 118 | 120 |
import java.lang.reflect.*; |
| 119 | 121 |
import java.util.*; |
| 122 |
import java.util.concurrent.*; |
|
| 120 | 123 |
import java.util.concurrent.atomic.*; |
| 121 | 124 |
import java.util.logging.*; |
| 122 | 125 | |
| ... | ... | |
| 253 | 256 |
|
| 254 | 257 |
/** Instance of compiled expression custom subclass */ |
| 255 | 258 |
private CompiledExpression compiled = null; |
| 259 | ||
| 260 |
/** |
|
| 261 |
* Compilers restored from the classes generated when FWD was built, keyed by the whole of the |
|
| 262 |
* index entry they were built from -- class, return type and both slot maps. See |
|
| 263 |
* {@link #restore} for why the class name alone is not a sufficient key.
|
|
| 264 |
*/ |
|
| 265 |
private static final Map<String, Compiler> RESTORED = new ConcurrentHashMap<>(); |
|
| 256 | 266 |
|
| 257 | 267 |
/** Constant for rounding floating point calculations to fixed precision */ |
| 258 | 268 |
private double roundingConstant = 0.0; |
| ... | ... | |
| 292 | 302 |
* to be compiled, including the expression text in infix |
| 293 | 303 |
* notation, and runtime flags which impact the behavior of |
| 294 | 304 |
* the compiled expression. |
| 305 |
* @param scope |
|
| 306 |
* Scope in force for this compilation. It must be the scope the caller resolved |
|
| 307 |
* before this call, not one re-derived here, so that the lookup and the publication |
|
| 308 |
* agree on the key. |
|
| 295 | 309 |
* |
| 296 | 310 |
* @return Instance of a custom subclass of |
| 297 | 311 |
* <code>CompiledExpression</code>. |
| ... | ... | |
| 299 | 313 |
* @throws CompilerException |
| 300 | 314 |
* if any error occurs during compilation of the expression. |
| 301 | 315 |
*/ |
| 302 |
static CompiledExpression compile(Expression expression) |
|
| 316 |
static CompiledExpression compile(Expression expression, Scope scope)
|
|
| 303 | 317 |
throws CompilerException |
| 304 | 318 |
{
|
| 305 |
// Check if there is already a compiler for this expression. |
|
| 306 |
Compiler compiler = expression.getCompiler(); |
|
| 319 |
// Check if there is already a compiler for this expression in this scope. The lookup is |
|
| 320 |
// scope aware because a generated class is specific to the scope which compiled it: the |
|
| 321 |
// types of the variables it reads are frozen into its bytecode. |
|
| 322 |
Compiler compiler = expression.getCompiler(scope); |
|
| 307 | 323 |
if (compiler == null) |
| 308 | 324 |
{
|
| 309 | 325 |
compiler = new Compiler(expression); |
| 310 | 326 |
} |
| 311 | 327 |
CompiledExpression compiled = compiler.process(expression.getResolver()); |
| 312 |
expression.putCompiler(compiler); |
|
| 328 |
expression.putCompiler(compiler, scope);
|
|
| 313 | 329 |
return compiled; |
| 314 | 330 |
} |
| 315 | 331 |
|
| ... | ... | |
| 353 | 369 |
CompiledExpression process(SymbolResolver resolver) |
| 354 | 370 |
throws CompilerException |
| 355 | 371 |
{
|
| 356 |
// Create a class file object into which to install execute method. |
|
| 357 |
int myIndex = classCount.getAndIncrement(); // reserve atomically upfront |
|
| 358 |
classFile = new ClassFile( |
|
| 359 |
PKG_PREFIX + CLASS_NAME_PREFIX + myIndex, |
|
| 360 |
CLASS_COMPILEDEXPRESSION |
|
| 361 |
); |
|
| 362 |
|
|
| 363 |
// Get constant pool, which is needed during assembly and |
|
| 364 |
// compilation. |
|
| 365 |
constantPool = classFile.getConstantPool(); |
|
| 366 |
|
|
| 367 | 372 |
try |
| 368 | 373 |
{
|
| 369 | 374 |
// Parse infix expression into AST. |
| ... | ... | |
| 378 | 383 |
ExpressionParser.visit(tree, 0); |
| 379 | 384 |
} |
| 380 | 385 |
|
| 386 |
// Everything the generated code depends on is resolved at this point and nothing has been |
|
| 387 |
// assembled yet, which is both where the compilation can be described and therefore where |
|
| 388 |
// a name derived from that description can be chosen. Only the generator needs such a |
|
| 389 |
// name; an ordinary run keeps the sequential naming, and the two live in different |
|
| 390 |
// packages so they can never collide. |
|
| 391 |
String internal; |
|
| 392 |
|
|
| 393 |
if (AotGenerator.isGenerating()) |
|
| 394 |
{
|
|
| 395 |
internal = AotGenerator.internalName(signature()); |
|
| 396 |
} |
|
| 397 |
else |
|
| 398 |
{
|
|
| 399 |
internal = PKG_PREFIX + CLASS_NAME_PREFIX + classCount.getAndIncrement(); |
|
| 400 |
} |
|
| 401 |
|
|
| 402 |
// Create a class file object into which to install execute method. Note that ClassFile |
|
| 403 |
// takes the internal, slash separated form, which is what ends up in the constant pool. |
|
| 404 |
classFile = new ClassFile(internal, CLASS_COMPILEDEXPRESSION); |
|
| 405 |
|
|
| 406 |
// Get constant pool, which is needed during assembly and |
|
| 407 |
// compilation. |
|
| 408 |
constantPool = classFile.getConstantPool(); |
|