Stateless parser API (#11147)

Stateless (static) parser interface. Buffer-reuse optimization is now hidden within `Parser` implementation. Fixes #11121 and prevents similar bugs.

# Important Notes
- Also simplify `EnsoParser` API, exposing only a higher-level interface.
This commit is contained in:
Kaz Wesley 2024-09-27 08:58:02 -07:00 committed by GitHub
parent e6b904d012
commit 289198127f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 396 additions and 373 deletions

View File

@ -6,6 +6,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.TreeSet;
import org.enso.compiler.core.EnsoParser;
import org.enso.compiler.core.ir.module.scope.imports.Polyglot;
import org.enso.pkg.PackageManager$;
import org.graalvm.nativeimage.hosted.Feature;
@ -45,14 +46,14 @@ public final class EnsoLibraryFeature implements Feature {
*/
var classes = new TreeSet<String>();
try (var parser = new org.enso.compiler.core.EnsoParser()) {
try {
for (var p : libs) {
var result = PackageManager$.MODULE$.Default().loadPackage(p.toFile());
if (result.isSuccess()) {
var pkg = result.get();
for (var src : pkg.listSourcesJava()) {
var code = Files.readString(src.file().toPath());
var ir = parser.compile(code);
var ir = EnsoParser.compile(code);
for (var imp : asJava(ir.imports())) {
if (imp instanceof Polyglot poly && poly.entity() instanceof Polyglot.Java entity) {
var name = new StringBuilder(entity.getJavaName());

View File

@ -35,6 +35,7 @@ import org.enso.compiler.phase.exports.{
ExportsResolution
}
import org.enso.syntax2.Tree
import org.enso.syntax2.Parser
import java.io.PrintStream
import java.util.concurrent.{
@ -69,7 +70,6 @@ class Compiler(
if (config.outputRedirect.isDefined)
new PrintStream(config.outputRedirect.get)
else context.getOut
private lazy val ensoCompiler: EnsoParser = new EnsoParser()
/** Java accessor */
def getConfig(): CompilerConfig = config
@ -598,11 +598,8 @@ class Compiler(
)
val src = context.getCharacters(module)
val idMap = context.getIdMap(module)
val tree = ensoCompiler.parse(src)
val expr =
if (idMap == null) ensoCompiler.generateIR(tree)
else ensoCompiler.generateModuleIr(tree, idMap.values)
val idMap = Option(context.getIdMap(module))
val expr = EnsoParser.compile(src, idMap.map(_.values).orNull)
val exprWithModuleExports =
if (context.isSynthetic(module))
@ -685,9 +682,8 @@ class Compiler(
inlineContext: InlineContext
): Option[(InlineContext, Expression)] = {
val newContext = inlineContext.copy(freshNameSupply = Some(freshNameSupply))
val tree = ensoCompiler.parse(srcString)
ensoCompiler.generateIRInline(tree).map { ir =>
EnsoParser.compileInline(srcString).map { ir =>
val compilerOutput = runCompilerPhasesInline(ir, newContext)
runErrorHandlingInline(compilerOutput, newContext)
(newContext, compilerOutput)
@ -700,7 +696,7 @@ class Compiler(
* @return A Tree representation of `source`
*/
def parseInline(source: CharSequence): Tree =
ensoCompiler.parse(source)
Parser.parse(source)
/** Enhances the provided IR with import/export statements for the provided list
* of fully qualified names of modules. The statements are considered to be "synthetic" i.e. compiler-generated.

View File

@ -11,8 +11,6 @@ import org.enso.compiler.data.CompilerConfig
import org.enso.common.CompilationStage
import org.enso.compiler.phase.exports.ExportsResolution
import scala.util.Using
/** A phase responsible for initializing the builtins' IR from the provided
* source.
*/
@ -44,9 +42,7 @@ object BuiltinsIrBuilder {
freshNameSupply = Some(freshNameSupply),
compilerConfig = CompilerConfig(warningsEnabled = false)
)
val initialIr = Using(new EnsoParser) { compiler =>
compiler.compile(module.getCharacters)
}.get
val initialIr = EnsoParser.compile(module.getCharacters)
val irAfterModDiscovery = passManager.runPassesOnModule(
initialIr,
moduleContext,

View File

@ -13,7 +13,6 @@ import org.enso.text.editing.{IndexedSource, TextEditor}
import java.util.UUID
import scala.collection.mutable
import scala.util.Using
/** The changeset of a module containing the computed list of invalidated
* expressions.
@ -97,14 +96,12 @@ final class ChangesetBuilder[A: TextEditor: IndexedSource](
}
val source = Source.newBuilder("enso", value, null).build
Using(new EnsoParser) { compiler =>
compiler
.generateIRInline(compiler.parse(source.getCharacters()))
EnsoParser
.compileInline(source.getCharacters())
.flatMap(_ match {
case ir: Literal => Some(ir.setLocation(oldIr.location))
case _ => None
})
}.get
}
oldIr match {

View File

@ -21,42 +21,6 @@ import org.enso.common.CompilationStage
trait CompilerTestSetup {
// === IR Utilities =========================================================
/** An extension method to allow converting string source code to IR as a
* module.
*
* @param source the source code to convert
*/
implicit private class ToIrModule(source: String) {
/** Converts program text to a top-level Enso module.
*
* @return the [[IR]] representing [[source]]
*/
def toIrModule: Module = {
val compiler = new EnsoParser()
try compiler.compile(source)
finally compiler.close()
}
}
/** An extension method to allow converting string source code to IR as an
* expression.
*
* @param source the source code to convert
*/
implicit private class ToIrExpression(source: String) {
/** Converts the program text to an Enso expression.
*
* @return the [[IR]] representing [[source]], if it is a valid expression
*/
def toIrExpression: Option[Expression] = {
val compiler = new EnsoParser()
try compiler.generateIRInline(compiler.parse(source))
finally compiler.close()
}
}
/** Provides an extension method allowing the running of a specified list of
* passes on the provided IR.
*
@ -112,7 +76,7 @@ trait CompilerTestSetup {
* @return IR appropriate for testing the alias analysis pass as a module
*/
def preprocessModule(implicit moduleContext: ModuleContext): Module = {
source.toIrModule.runPasses(passManager, moduleContext)
EnsoParser.compile(source).runPasses(passManager, moduleContext)
}
/** Translates the source code into appropriate IR for testing this pass
@ -123,7 +87,9 @@ trait CompilerTestSetup {
def preprocessExpression(implicit
inlineContext: InlineContext
): Option[Expression] = {
source.toIrExpression.map(_.runPasses(passManager, inlineContext))
EnsoParser
.compileInline(source)
.map(_.runPasses(passManager, inlineContext))
}
}

View File

@ -11,25 +11,10 @@ import java.util.function.Function;
import org.enso.compiler.core.EnsoParser;
import org.enso.compiler.core.IR;
import org.enso.compiler.core.ir.Module;
import org.junit.AfterClass;
import org.junit.BeforeClass;
public abstract class CompilerTests {
protected static EnsoParser ensoCompiler;
@BeforeClass
public static void initEnsoParser() {
ensoCompiler = new EnsoParser();
}
@AfterClass
public static void closeEnsoParser() throws Exception {
ensoCompiler.close();
}
protected static Module parse(CharSequence code) {
Module ir = ensoCompiler.compile(code);
Module ir = EnsoParser.compile(code);
assertNotNull("IR was generated", ir);
return ir;
}

View File

@ -19,25 +19,10 @@ import org.enso.compiler.core.ir.Function;
import org.enso.compiler.core.ir.Name;
import org.enso.compiler.core.ir.expression.Comment;
import org.enso.compiler.core.ir.module.scope.Definition;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import scala.Function1;
public class VectorArraySignatureTest {
private static EnsoParser ensoCompiler;
@BeforeClass
public static void initEnsoParser() {
ensoCompiler = new EnsoParser();
}
@AfterClass
public static void closeEnsoParser() throws Exception {
ensoCompiler.close();
ensoCompiler = null;
}
@Test
public void testParseVectorAndArray() throws Exception {
var p = Paths.get("../../distribution/").toFile().getCanonicalFile();
@ -81,8 +66,7 @@ public class VectorArraySignatureTest {
var vectorSrc = Files.readString(vectorAndArray[1]);
var arrayIR =
ensoCompiler
.compile(arraySrc)
EnsoParser.compile(arraySrc)
.preorder()
.filter(
(v) -> {
@ -95,8 +79,7 @@ public class VectorArraySignatureTest {
})
.head();
var vectorIR =
ensoCompiler
.compile(vectorSrc)
EnsoParser.compile(vectorSrc)
.preorder()
.filter(
(v) -> {

View File

@ -31,42 +31,6 @@ trait CompilerTest extends AnyWordSpecLike with Matchers with CompilerRunner
trait CompilerRunner {
// === IR Utilities =========================================================
/** An extension method to allow converting string source code to IR as a
* module.
*
* @param source the source code to convert
*/
implicit class ToIrModule(source: String) {
/** Converts program text to a top-level Enso module.
*
* @return the [[IR]] representing [[source]]
*/
def toIrModule: Module = {
val compiler = new EnsoParser()
try compiler.compile(source)
finally compiler.close()
}
}
/** An extension method to allow converting string source code to IR as an
* expression.
*
* @param source the source code to convert
*/
implicit class ToIrExpression(source: String) {
/** Converts the program text to an Enso expression.
*
* @return the [[IR]] representing [[source]], if it is a valid expression
*/
def toIrExpression: Option[Expression] = {
val compiler = new EnsoParser()
try compiler.generateIRInline(compiler.parse(source))
finally compiler.close()
}
}
/** Provides an extension method allowing the running of a specified list of
* passes on the provided IR.
*
@ -137,7 +101,7 @@ trait CompilerRunner {
* @return IR appropriate for testing the alias analysis pass as a module
*/
def preprocessModule(implicit moduleContext: ModuleContext): Module = {
source.toIrModule.runPasses(passManager, moduleContext)
EnsoParser.compile(source).runPasses(passManager, moduleContext)
}
/** Translates the source code into appropriate IR for testing this pass
@ -148,7 +112,9 @@ trait CompilerRunner {
def preprocessExpression(implicit
inlineContext: InlineContext
): Option[Expression] = {
source.toIrExpression.map(_.runPasses(passManager, inlineContext))
EnsoParser
.compileInline(source)
.map(_.runPasses(passManager, inlineContext))
}
}

View File

@ -2,7 +2,7 @@ package org.enso.compiler.test.pass.resolve
import org.enso.compiler.Passes
import org.enso.compiler.context.{FreshNameSupply, ModuleContext}
import org.enso.compiler.core.IR
import org.enso.compiler.core.{EnsoParser, IR}
import org.enso.compiler.core.Implicits.AsMetadata
import org.enso.compiler.core.ir.Expression
import org.enso.compiler.core.ir.Function
@ -84,7 +84,7 @@ class GlobalNamesTest extends CompilerTest {
|add_one x = x + 1
|
|""".stripMargin
val parsed = code.toIrModule
val parsed = EnsoParser.compile(code)
val moduleMapped = passManager.runPassesOnModule(parsed, ctx, group1)
ModuleTestUtils.unsafeSetIr(both._2, moduleMapped)

View File

@ -1351,6 +1351,17 @@ class RuntimeVisualizationsTest extends AnyFlatSpec with Matchers {
)
)
val attachVisualizationResponses =
context.receiveNIgnoreExpressionUpdates(4)
attachVisualizationResponses.filter(
_.payload.isInstanceOf[Api.VisualizationAttached]
) shouldEqual List(
Api.Response(requestId, Api.VisualizationAttached()),
Api.Response(requestId, Api.VisualizationAttached())
)
// Modify the file
context.send(
Api.Request(
Api.EditFileNotification(
@ -1367,23 +1378,17 @@ class RuntimeVisualizationsTest extends AnyFlatSpec with Matchers {
)
)
val responses =
context.receiveNIgnoreExpressionUpdates(7)
val editFileResponses =
context.receiveNIgnoreExpressionUpdates(3)
responses should contain allOf (
Api.Response(requestId, Api.VisualizationAttached()),
editFileResponses should contain(
context.executionComplete(contextId)
)
responses.filter(
_.payload.isInstanceOf[Api.VisualizationAttached]
) shouldEqual List(
Api.Response(requestId, Api.VisualizationAttached()),
Api.Response(requestId, Api.VisualizationAttached())
)
val visualizationUpdatesResponses =
responses.filter(_.payload.isInstanceOf[Api.VisualizationUpdate])
(attachVisualizationResponses ::: editFileResponses).filter(
_.payload.isInstanceOf[Api.VisualizationUpdate]
)
val expectedExpressionId = context.Main.idMainX
val visualizationUpdates = visualizationUpdatesResponses.map(
_.payload.asInstanceOf[Api.VisualizationUpdate]

View File

@ -6,48 +6,34 @@ import org.enso.compiler.core.ir.Expression;
import org.enso.compiler.core.ir.Location;
import org.enso.compiler.core.ir.Module;
import org.enso.syntax2.Parser;
import org.enso.syntax2.Tree;
public final class EnsoParser implements AutoCloseable {
private final Parser parser;
public final class EnsoParser {
private EnsoParser() {}
public EnsoParser() {
Parser p;
try {
p = Parser.create();
} catch (LinkageError err) {
err.printStackTrace();
throw err;
}
this.parser = p;
public static Module compile(CharSequence src) {
return compile(src, null);
}
@Override
public void close() throws Exception {
if (parser != null) {
parser.close();
public static Module compile(CharSequence src, Map<Location, UUID> idMap) {
var tree = Parser.parse(src);
var treeToIr = TreeToIr.MODULE;
if (idMap != null) {
treeToIr = new TreeToIr(idMap);
}
return treeToIr.translate(tree);
}
public Module compile(CharSequence src) {
var tree = parser.parse(src);
return generateIR(tree);
public static scala.Option<Expression> compileInline(CharSequence src) {
var tree = Parser.parse(src);
return TreeToIr.MODULE.translateInline(tree);
}
public Tree parse(CharSequence src) {
return parser.parse(src);
}
public Module generateIR(Tree t) {
return TreeToIr.MODULE.translate(t);
}
public Module generateModuleIr(Tree t, Map<Location, UUID> idMap) {
var treeToIr = new TreeToIr(idMap);
return treeToIr.translate(t);
}
public scala.Option<Expression> generateIRInline(Tree t) {
return TreeToIr.MODULE.translateInline(t);
/**
* Free retained state of all parsers. Parser buffers are retained per-thread for reuse; this
* function drops those reusable buffers. If the parser is used again after this call, it will
* allocate new buffers as needed.
*/
public static void freeAll() {
Parser.freeAll();
}
}

View File

@ -0,0 +1,38 @@
package org.enso.compiler.core;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ForkJoinPool;
import org.enso.compiler.core.ir.Module;
import org.junit.Test;
public class EnsoParserMultiThreadedTest {
@Test
public void stressLoadFromManyThreads() throws Exception {
List<Callable<Module>> cases = new ArrayList<>();
final var testCases = 1000;
for (var i = 0; i < 2 * testCases; i++) {
var number = i % testCases;
var code =
"""
from Standard.Base import all
main = %n
"""
.replace("%n", "" + number);
cases.add(
() -> {
return EnsoParser.compile(code);
});
}
var results = ForkJoinPool.commonPool().invokeAll(cases);
for (var i = 0; i < testCases; i++) {
var r1 = results.get(i).get();
var r2 = results.get(testCases + i).get();
EnsoParserTest.assertIR("Run #" + i + " should produce identical IR", r1, r2);
}
}
}

View File

@ -19,28 +19,10 @@ import java.util.function.Function;
import org.enso.compiler.core.ir.Module;
import org.enso.compiler.core.ir.expression.Error;
import org.enso.compiler.core.ir.module.scope.definition.Method;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import scala.jdk.javaapi.CollectionConverters;
public class EnsoParserTest {
private static EnsoParser ensoCompiler;
@BeforeClass
public static void initEnsoParser() {
try {
ensoCompiler = new EnsoParser();
} catch (LinkageError e) {
throw new AssertionError(e);
}
}
@AfterClass
public static void closeEnsoParser() throws Exception {
if (ensoCompiler != null) ensoCompiler.close();
}
@Test
public void testParseMain7Foo() {
parseTest("""
@ -1529,7 +1511,9 @@ public class EnsoParserTest {
}
private static Module compile(String code) {
return compile(ensoCompiler, code);
var ir = EnsoParser.compile(code);
assertNotNull("IR was generated", ir);
return ir;
}
private void expectNoErrorsInIr(Module moduleIr) {
@ -1544,12 +1528,6 @@ public class EnsoParserTest {
});
}
public static Module compile(EnsoParser c, String code) {
var ir = c.compile(code);
assertNotNull("IR was generated", ir);
return ir;
}
static void assertIR(String msg, Module old, Module now) throws IOException {
Function<IR, String> filter = f -> simplifyIR(f, true, true, false);
String ir1 = filter.apply(old);

View File

@ -42,6 +42,7 @@ import java.util.logging.Level;
import org.enso.common.LanguageInfo;
import org.enso.common.RuntimeOptions;
import org.enso.compiler.Compiler;
import org.enso.compiler.core.EnsoParser;
import org.enso.compiler.data.CompilerConfig;
import org.enso.compiler.dump.IRDumper;
import org.enso.distribution.DistributionManager;
@ -296,6 +297,7 @@ public final class EnsoContext {
guestJava = null;
topScope = null;
hostClassLoader.close();
EnsoParser.freeAll();
}
private boolean shouldAssertionsBeEnabled() {

View File

@ -149,7 +149,6 @@ public final class Ydoc implements AutoCloseable {
public void close() throws Exception {
executor.shutdownNow();
executor.awaitTermination(3, TimeUnit.SECONDS);
parser.close();
if (context != null) {
context.close(true);
}

View File

@ -9,7 +9,7 @@ import org.graalvm.polyglot.proxy.ProxyExecutable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ParserPolyfill implements AutoCloseable, ProxyExecutable, Polyfill {
public final class ParserPolyfill implements ProxyExecutable, Polyfill {
private static final Logger log = LoggerFactory.getLogger(ParserPolyfill.class);
@ -19,21 +19,10 @@ public final class ParserPolyfill implements AutoCloseable, ProxyExecutable, Pol
private static final String PARSER_JS = "parser.js";
private final Parser parser;
public ParserPolyfill() {
Parser p;
try {
p = Parser.create();
} catch (LinkageError e) {
log.error("Failed to create parser", e);
throw e;
}
this.parser = p;
}
public ParserPolyfill() {}
@Override
public final void initialize(Context ctx) {
public void initialize(Context ctx) {
Source parserJs =
Source.newBuilder("js", ParserPolyfill.class.getResource(PARSER_JS)).buildLiteral();
@ -50,7 +39,7 @@ public final class ParserPolyfill implements AutoCloseable, ProxyExecutable, Pol
case PARSE_TREE -> {
var input = arguments[1].asString();
yield parser.parseInputLazy(input);
yield Parser.parseInputLazy(input);
}
case XX_HASH_128 -> {
@ -62,15 +51,10 @@ public final class ParserPolyfill implements AutoCloseable, ProxyExecutable, Pol
case IS_IDENT_OR_OPERATOR -> {
var input = arguments[1].asString();
yield parser.isIdentOrOperator(input);
yield Parser.isIdentOrOperator(input);
}
default -> throw new IllegalStateException(command);
};
}
@Override
public void close() {
parser.close();
}
}

View File

@ -37,7 +37,6 @@ public class ParserPolyfillTest extends ExecutorSetup {
public void tearDown() throws InterruptedException {
super.tearDown();
context.close();
parser.close();
}
@Test

View File

@ -1,5 +1,6 @@
module org.enso.syntax {
requires org.slf4j;
requires org.graalvm.nativeimage;
exports org.enso.syntax2;
}

View File

@ -0,0 +1,60 @@
package org.enso.syntax2;
import java.lang.ref.*;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
final class FinalizationManager {
private final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
private final Set<FinalizationReference> finalizers =
Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
/**
* Associate the given callback with an object so that if the object is freed, the callback will
* be scheduled to be run. Note that the callback is not guaranteed to be executed, e.g. if the
* process exits first.
*
* @param referent Object whose lifetime determines when the finalizer will be run.
* @param finalize Callback to run after {@code referent} has been garbage-collected.
*/
<T> void attachFinalizer(T referent, Runnable finalize) {
finalizers.add(new FinalizationReference(referent, finalize, referenceQueue));
}
void runPendingFinalizers() {
var ref = referenceQueue.poll();
while (ref != null) {
runFinalizer(ref);
ref = referenceQueue.poll();
}
}
/**
* @return The finalizers that have been registered, and have not yet been run.
* <p>This does not de-register the finalizers; they will still be run as usual after their
* reference objects become unreachable.
*/
Iterable<Runnable> getRegisteredFinalizers() {
synchronized (finalizers) {
return finalizers.stream().map(ref -> ref.finalize).toList();
}
}
private void runFinalizer(Reference<?> ref) {
if (ref instanceof FinalizationReference) {
var finalizationReference = (FinalizationReference) ref;
finalizationReference.finalize.run();
finalizers.remove(finalizationReference);
}
}
private static class FinalizationReference extends PhantomReference<Object> {
final Runnable finalize;
FinalizationReference(Object referent, Runnable finalize, ReferenceQueue<? super Object> q) {
super(referent, q);
this.finalize = finalize;
}
}
}

View File

@ -62,15 +62,7 @@ final class Message {
}
java.util.UUID getUuid(long nodeOffset, long nodeLength) {
long high = Parser.getUuidHigh(metadata, nodeOffset, nodeLength);
long low = Parser.getUuidLow(metadata, nodeOffset, nodeLength);
if (high == 0 && low == 0) {
// The native interface uses the Nil UUID value as a marker to indicate that no UUID was
// attached.
// The Nil UUID will never collide with a real UUID generated by any scheme.
return null;
}
return new java.util.UUID(high, low);
return Parser.getUuid(metadata, nodeOffset, nodeLength);
}
long position() {

View File

@ -6,9 +6,71 @@ import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import org.graalvm.nativeimage.ImageInfo;
import org.slf4j.LoggerFactory;
public final class Parser implements AutoCloseable {
public final class Parser {
private Parser() {}
public static String getWarningMessage(Warning warning) {
return getWarningTemplate(warning.getId());
}
public static long isIdentOrOperator(CharSequence input) {
return getWorker().isIdentOrOperator(input);
}
public static ByteBuffer parseInputLazy(CharSequence input) {
return getWorker().parseInputLazy(input);
}
public static Tree parse(CharSequence input) {
return getWorker().parse(input);
}
public static UUID getUuid(long metadata, long nodeOffset, long nodeLength) {
long high = getUuidHigh(metadata, nodeOffset, nodeLength);
long low = getUuidLow(metadata, nodeOffset, nodeLength);
if (high == 0 && low == 0) {
// The native interface uses the Nil UUID value as a marker to indicate that no UUID was
// attached.
// The Nil UUID will never collide with a real UUID generated by any scheme.
return null;
}
return new UUID(high, low);
}
/* Worker-thread state */
private static final FinalizationManager finalizationManager = new FinalizationManager();
private static Worker getWorker() {
finalizationManager.runPendingFinalizers();
return threadWorker.get();
}
private static Worker createWorker() {
var worker = Worker.create();
if (!ImageInfo.inImageBuildtimeCode()) {
// At build-time, we eagerly free parser buffers; runtime should start out with an empty
// `finalizationManager`.
finalizationManager.attachFinalizer(worker, worker.finalizer());
threadWorker.set(worker);
}
return worker;
}
private static final ThreadLocal<Worker> threadWorker =
ThreadLocal.withInitial(Parser::createWorker);
public static void freeAll() {
for (var finalizer : finalizationManager.getRegisteredFinalizers()) finalizer.run();
}
private static class Worker {
private static void initializeLibraries() {
try {
System.loadLibrary("enso_parser");
@ -77,12 +139,89 @@ public final class Parser implements AutoCloseable {
return false;
}
private long stateUnlessClosed;
private final AtomicLong state = new AtomicLong(0);
private Parser(long stateIn) {
stateUnlessClosed = stateIn;
private Worker() {}
private static class Finalizer implements Runnable {
private final AtomicLong state;
private Finalizer(AtomicLong state) {
this.state = state;
}
@Override
public void run() {
freeState(state.getAndSet(0));
}
}
Runnable finalizer() {
return new Finalizer(state);
}
static Worker create() {
initializeLibraries();
return new Worker();
}
private <T> T withState(Function<Long, T> stateConsumer) {
// Take the state for the duration of the operation so that it can't be freed by another
// thread.
var privateState = state.getAndSet(0);
if (privateState == 0) privateState = allocState();
var result = stateConsumer.apply(privateState);
if (ImageInfo.inImageBuildtimeCode()) {
// At build-time, eagerly free buffers. We don't want them included in the heap snapshot!
freeState(privateState);
} else {
// We don't need to check the value before setting here: A state may be freed by another
// thread, but is only allocated by its associated `Worker`, so after taking it above, the
// shared value remains 0 until we restore it.
state.set(privateState);
}
return result;
}
long isIdentOrOperator(CharSequence input) {
byte[] inputBytes = input.toString().getBytes(StandardCharsets.UTF_8);
ByteBuffer inputBuf = ByteBuffer.allocateDirect(inputBytes.length);
inputBuf.put(inputBytes);
return Parser.isIdentOrOperator(inputBuf);
}
ByteBuffer parseInputLazy(CharSequence input) {
byte[] inputBytes = input.toString().getBytes(StandardCharsets.UTF_8);
ByteBuffer inputBuf = ByteBuffer.allocateDirect(inputBytes.length);
inputBuf.put(inputBytes);
return withState(state -> parseTreeLazy(state, inputBuf));
}
Tree parse(CharSequence input) {
byte[] inputBytes = input.toString().getBytes(StandardCharsets.UTF_8);
ByteBuffer inputBuf = ByteBuffer.allocateDirect(inputBytes.length);
inputBuf.put(inputBytes);
return withState(
state -> {
var serializedTree = parseTree(state, inputBuf);
var base = getLastInputBase(state);
var metadata = getMetadata(state);
serializedTree.order(ByteOrder.LITTLE_ENDIAN);
var message = new Message(serializedTree, input, base, metadata);
try {
return Tree.deserialize(message);
} catch (BufferUnderflowException | IllegalArgumentException e) {
LoggerFactory.getLogger(this.getClass())
.error("Unrecoverable parser failure for: {}", input, e);
throw e;
}
});
}
}
/* JNI declarations */
private static native long allocState();
private static native void freeState(long state);
@ -99,66 +238,7 @@ public final class Parser implements AutoCloseable {
private static native String getWarningTemplate(int warningId);
static native long getUuidHigh(long metadata, long codeOffset, long codeLength);
private static native long getUuidHigh(long metadata, long codeOffset, long codeLength);
static native long getUuidLow(long metadata, long codeOffset, long codeLength);
public static Parser create() {
initializeLibraries();
var state = allocState();
return new Parser(state);
}
public long isIdentOrOperator(CharSequence input) {
byte[] inputBytes = input.toString().getBytes(StandardCharsets.UTF_8);
ByteBuffer inputBuf = ByteBuffer.allocateDirect(inputBytes.length);
inputBuf.put(inputBytes);
return isIdentOrOperator(inputBuf);
}
private long getState() {
if (stateUnlessClosed != 0) {
return stateUnlessClosed;
} else {
throw new IllegalStateException("Parser used after close()");
}
}
public ByteBuffer parseInputLazy(CharSequence input) {
var state = getState();
byte[] inputBytes = input.toString().getBytes(StandardCharsets.UTF_8);
ByteBuffer inputBuf = ByteBuffer.allocateDirect(inputBytes.length);
inputBuf.put(inputBytes);
return parseTreeLazy(state, inputBuf);
}
public Tree parse(CharSequence input) {
var state = getState();
byte[] inputBytes = input.toString().getBytes(StandardCharsets.UTF_8);
ByteBuffer inputBuf = ByteBuffer.allocateDirect(inputBytes.length);
inputBuf.put(inputBytes);
var serializedTree = parseTree(state, inputBuf);
var base = getLastInputBase(state);
var metadata = getMetadata(state);
serializedTree.order(ByteOrder.LITTLE_ENDIAN);
var message = new Message(serializedTree, input, base, metadata);
try {
return Tree.deserialize(message);
} catch (BufferUnderflowException | IllegalArgumentException e) {
LoggerFactory.getLogger(this.getClass())
.error("Unrecoverable parser failure for: {}", input, e);
throw e;
}
}
public static String getWarningMessage(Warning warning) {
return getWarningTemplate(warning.getId());
}
@Override
public void close() {
freeState(stateUnlessClosed);
stateUnlessClosed = 0;
}
private static native long getUuidLow(long metadata, long codeOffset, long codeLength);
}

View File

@ -28,7 +28,9 @@ fn main() {
println!("import java.nio.ByteOrder;");
println!();
println!("class GeneratedFormatTests {{");
println!(" private static final Object INIT = {package}.Parser.create();");
// Force the parser to load its shared library. `parse` handles this because usually it is the
// entry point to the class, but we're doing low-level operations directly.
println!(" private static final Object INIT = {package}.Parser.parse(\"\");");
println!(" private static java.util.Vector<byte[]> accept;");
println!(" private static java.util.Vector<byte[]> reject;");
for (i, case) in cases.accept.iter().enumerate() {

View File

@ -294,3 +294,12 @@ struct State {
output: Vec<u8>,
metadata: Option<enso_parser::metadata::Metadata>,
}
mod static_trait_check {
fn assert_send<T: Send>() {}
fn assert_state_send() {
// Require `State` to be `Send`-safe so that in Java it can be deallocated (or potentially
// reused) by any thread.
assert_send::<super::State>()
}
}

View File

@ -27,12 +27,11 @@ public final class EnsoErrorProvider implements ErrorProvider {
try {
if (ctx.errorKind() == Kind.ERRORS) {
LOG.log(Level.FINE, "Processing errors for {0}", ctx.file().getPath());
var parser = new EnsoParser();
var text = toText(ctx);
Function1<IdentifiedLocation, String> where = (loc) -> {
return text.substring(loc.start(), loc.end());
};
var moduleIr = parser.compile(text);
var moduleIr = EnsoParser.compile(text);
moduleIr.preorder().foreach((p) -> {
if (p instanceof Syntax err && err.location().isDefined()) {
var loc = err.location().get();

View File

@ -32,9 +32,8 @@ public final class EnsoStructure implements StructureProvider {
}
var arr = new ArrayList<StructureElement>();
try {
var parser = new EnsoParser();
var text = dcmnt.getText(0, dcmnt.getLength());
var moduleIr = parser.compile(text);
var moduleIr = EnsoParser.compile(text);
var it = moduleIr.bindings().iterator();
collectStructure(file, arr, it);
return arr;