Project

General

Profile

aot.patch

Teodor Gorghe, 09/25/2026 02:29 AM

Download (99.2 KB)

View differences:

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();
... This diff was truncated because it exceeds the maximum size that can be displayed.