From 02b3c92912da4a9f857b172275df97724dacba92 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Wed, 18 Oct 2023 19:24:47 +0200 Subject: [PATCH] JS: use native string to represent internals of java.lang.String --- .../org/teavm/classlib/impl/JCLPlugin.java | 14 + .../impl/string/DefaultStringTransformer.java | 280 ++++++++++++++++++ .../string/JSStringConstructorGenerator.java | 31 ++ .../impl/string/JSStringInjector.java | 234 +++++++++++++++ .../impl/string/JSStringTransformer.java | 44 +++ ...rator.java => StringNativeDependency.java} | 13 +- .../org/teavm/classlib/java/lang/TString.java | 214 ++++++++----- .../backend/javascript/JavaScriptTarget.java | 3 +- .../javascript/rendering/Renderer.java | 3 +- .../javascript/rendering/RuntimeRenderer.java | 43 +-- .../org/teavm/backend/javascript/intern.js | 63 ++-- .../org/teavm/backend/javascript/runtime.js | 35 ++- .../teavm/html4j/JavaScriptConvGenerator.java | 4 +- .../org/teavm/jso/impl/JSNativeGenerator.java | 2 +- .../plugin/ResourceAccessorInjector.java | 2 +- .../teavm/classlib/java/lang/StringTest.java | 2 +- .../incremental/EntryPointGenerator.java | 3 +- .../teavm/incremental/IncrementalTest.java | 8 +- 18 files changed, 835 insertions(+), 163 deletions(-) create mode 100644 classlib/src/main/java/org/teavm/classlib/impl/string/DefaultStringTransformer.java create mode 100644 classlib/src/main/java/org/teavm/classlib/impl/string/JSStringConstructorGenerator.java create mode 100644 classlib/src/main/java/org/teavm/classlib/impl/string/JSStringInjector.java create mode 100644 classlib/src/main/java/org/teavm/classlib/impl/string/JSStringTransformer.java rename classlib/src/main/java/org/teavm/classlib/java/lang/{StringNativeGenerator.java => StringNativeDependency.java} (71%) diff --git a/classlib/src/main/java/org/teavm/classlib/impl/JCLPlugin.java b/classlib/src/main/java/org/teavm/classlib/impl/JCLPlugin.java index e52e21497..9a39e7e63 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/JCLPlugin.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/JCLPlugin.java @@ -29,6 +29,10 @@ import org.teavm.classlib.impl.currency.CurrencyHelper; import org.teavm.classlib.impl.lambda.LambdaMetafactorySubstitutor; import org.teavm.classlib.impl.record.ObjectMethodsSubstitutor; import org.teavm.classlib.impl.reflection.ReflectionTransformer; +import org.teavm.classlib.impl.string.DefaultStringTransformer; +import org.teavm.classlib.impl.string.JSStringConstructorGenerator; +import org.teavm.classlib.impl.string.JSStringInjector; +import org.teavm.classlib.impl.string.JSStringTransformer; import org.teavm.classlib.impl.tz.DateTimeZoneProvider; import org.teavm.classlib.impl.tz.DateTimeZoneProviderIntrinsic; import org.teavm.classlib.impl.tz.DateTimeZoneProviderPatch; @@ -186,6 +190,16 @@ public class JCLPlugin implements TeaVMPlugin { installMetadata(host.getService(MetadataRegistration.class)); host.add(new DeclaringClassDependencyListener()); applyTimeZoneDetection(host); + + var js = host.getExtension(TeaVMJavaScriptHost.class); + if (js != null) { + host.add(new JSStringTransformer()); + js.addInjectorProvider(new JSStringInjector()); + js.add(new MethodReference(String.class, "", Object.class, void.class), + new JSStringConstructorGenerator()); + } else { + host.add(new DefaultStringTransformer()); + } } private void applyTimeZoneDetection(TeaVMHost host) { diff --git a/classlib/src/main/java/org/teavm/classlib/impl/string/DefaultStringTransformer.java b/classlib/src/main/java/org/teavm/classlib/impl/string/DefaultStringTransformer.java new file mode 100644 index 000000000..5382dffd7 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/impl/string/DefaultStringTransformer.java @@ -0,0 +1,280 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.impl.string; + +import java.util.List; +import org.teavm.model.AccessLevel; +import org.teavm.model.ClassHolder; +import org.teavm.model.ClassHolderTransformer; +import org.teavm.model.ClassHolderTransformerContext; +import org.teavm.model.FieldHolder; +import org.teavm.model.FieldReference; +import org.teavm.model.MethodReference; +import org.teavm.model.Program; +import org.teavm.model.ValueType; +import org.teavm.model.instructions.ArrayElementType; +import org.teavm.model.instructions.ArrayLengthInstruction; +import org.teavm.model.instructions.ConstructArrayInstruction; +import org.teavm.model.instructions.GetElementInstruction; +import org.teavm.model.instructions.GetFieldInstruction; +import org.teavm.model.instructions.IntegerConstantInstruction; +import org.teavm.model.instructions.InvocationType; +import org.teavm.model.instructions.InvokeInstruction; +import org.teavm.model.instructions.PutFieldInstruction; +import org.teavm.model.instructions.UnwrapArrayInstruction; + +public class DefaultStringTransformer implements ClassHolderTransformer { + @Override + public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) { + if (cls.getName().equals("java.lang.String")) { + transformString(cls); + } + } + + private void transformString(ClassHolder cls) { + var fields = List.copyOf(cls.getFields()); + for (var field : fields) { + cls.removeField(field); + } + + var charactersField = new FieldHolder("characters"); + charactersField.setType(ValueType.arrayOf(ValueType.CHARACTER)); + charactersField.setLevel(AccessLevel.PRIVATE); + cls.addField(charactersField); + + for (var field : fields) { + cls.addField(field); + } + + for (var method : cls.getMethods()) { + if (method.getProgram() != null) { + transformProgram(method.getProgram()); + } + } + } + + private void transformProgram(Program program) { + for (var block : program.getBasicBlocks()) { + for (var instruction : block) { + if (!(instruction instanceof InvokeInstruction)) { + continue; + } + var invoke = (InvokeInstruction) instruction; + if (!invoke.getMethod().getClassName().equals("java.lang.String")) { + continue; + } + switch (invoke.getMethod().getName()) { + case "initWithEmptyChars": + replaceInitWithEmptyChars(invoke); + break; + case "borrowChars": + replaceBorrowChars(invoke); + break; + case "initWithCharArray": + replaceInitWithCharArray(invoke); + break; + case "takeCharArray": + replaceTakeCharArray(invoke); + break; + case "charactersLength": + replaceCharactersLength(invoke); + break; + case "charactersGet": + replaceCharactersGet(invoke); + break; + case "copyCharsToArray": + replaceCopyCharsToArray(invoke); + break; + case "fastCharArray": + replaceFastCharArray(invoke); + break; + } + } + } + } + + private void replaceInitWithEmptyChars(InvokeInstruction invoke) { + var program = invoke.getProgram(); + + var getField = new GetFieldInstruction(); + getField.setField(new FieldReference("java.lang.String", "EMPTY_CHARS")); + getField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + getField.setReceiver(program.createVariable()); + getField.setLocation(invoke.getLocation()); + invoke.insertNext(getField); + + var putField = new PutFieldInstruction(); + putField.setField(new FieldReference("java.lang.String", "characters")); + putField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + putField.setInstance(invoke.getInstance()); + putField.setValue(getField.getReceiver()); + putField.setLocation(invoke.getLocation()); + getField.insertNext(putField); + + invoke.delete(); + } + + private void replaceBorrowChars(InvokeInstruction invoke) { + var program = invoke.getProgram(); + + var getField = new GetFieldInstruction(); + getField.setField(new FieldReference("java.lang.String", "characters")); + getField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + getField.setInstance(invoke.getArguments().get(0)); + getField.setReceiver(program.createVariable()); + getField.setLocation(invoke.getLocation()); + invoke.insertNext(getField); + + var putField = new PutFieldInstruction(); + putField.setField(new FieldReference("java.lang.String", "characters")); + putField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + putField.setInstance(invoke.getInstance()); + putField.setValue(getField.getReceiver()); + putField.setLocation(invoke.getLocation()); + getField.insertNext(putField); + + invoke.delete(); + } + + private void replaceInitWithCharArray(InvokeInstruction invoke) { + var program = invoke.getProgram(); + + var createArray = new ConstructArrayInstruction(); + createArray.setItemType(ValueType.CHARACTER); + createArray.setSize(invoke.getArguments().get(2)); + createArray.setReceiver(program.createVariable()); + createArray.setLocation(invoke.getLocation()); + invoke.insertNext(createArray); + + var zero = new IntegerConstantInstruction(); + zero.setReceiver(program.createVariable()); + zero.setLocation(invoke.getLocation()); + createArray.insertNext(zero); + + var arrayCopy = new InvokeInstruction(); + arrayCopy.setType(InvocationType.SPECIAL); + arrayCopy.setMethod(new MethodReference(System.class, "arraycopy", Object.class, int.class, + Object.class, int.class, int.class, void.class)); + arrayCopy.setArguments(invoke.getArguments().get(0), invoke.getArguments().get(1), + createArray.getReceiver(), zero.getReceiver(), invoke.getArguments().get(2)); + zero.insertNext(arrayCopy); + + var putField = new PutFieldInstruction(); + putField.setField(new FieldReference("java.lang.String", "characters")); + putField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + putField.setInstance(program.variableAt(0)); + putField.setValue(createArray.getReceiver()); + putField.setLocation(invoke.getLocation()); + arrayCopy.insertNext(putField); + + invoke.delete(); + } + + private void replaceTakeCharArray(InvokeInstruction invoke) { + var putField = new PutFieldInstruction(); + putField.setField(new FieldReference("java.lang.String", "characters")); + putField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + putField.setInstance(invoke.getInstance()); + putField.setValue(invoke.getArguments().get(0)); + putField.setLocation(invoke.getLocation()); + invoke.replace(putField); + } + + private void replaceCharactersLength(InvokeInstruction invoke) { + var program = invoke.getProgram(); + + var getField = new GetFieldInstruction(); + getField.setField(new FieldReference("java.lang.String", "characters")); + getField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + getField.setInstance(invoke.getInstance()); + getField.setReceiver(program.createVariable()); + getField.setLocation(invoke.getLocation()); + invoke.insertNext(getField); + + var unwrapArray = new UnwrapArrayInstruction(ArrayElementType.CHAR); + unwrapArray.setArray(getField.getReceiver()); + unwrapArray.setReceiver(program.createVariable()); + unwrapArray.setLocation(invoke.getLocation()); + getField.insertNext(unwrapArray); + + var getLength = new ArrayLengthInstruction(); + getLength.setArray(unwrapArray.getReceiver()); + getLength.setReceiver(invoke.getReceiver()); + getLength.setLocation(invoke.getLocation()); + unwrapArray.insertNext(getLength); + + invoke.delete(); + } + + private void replaceCharactersGet(InvokeInstruction invoke) { + var program = invoke.getProgram(); + + var getField = new GetFieldInstruction(); + getField.setField(new FieldReference("java.lang.String", "characters")); + getField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + getField.setInstance(invoke.getInstance()); + getField.setReceiver(program.createVariable()); + getField.setLocation(invoke.getLocation()); + invoke.insertNext(getField); + + var unwrapArray = new UnwrapArrayInstruction(ArrayElementType.CHAR); + unwrapArray.setArray(getField.getReceiver()); + unwrapArray.setReceiver(program.createVariable()); + unwrapArray.setLocation(invoke.getLocation()); + getField.insertNext(unwrapArray); + + var getFromArray = new GetElementInstruction(ArrayElementType.CHAR); + getFromArray.setArray(unwrapArray.getReceiver()); + getFromArray.setReceiver(invoke.getReceiver()); + getFromArray.setIndex(invoke.getArguments().get(0)); + getFromArray.setLocation(invoke.getLocation()); + unwrapArray.insertNext(getFromArray); + + invoke.delete(); + } + + private void replaceCopyCharsToArray(InvokeInstruction invoke) { + var program = invoke.getProgram(); + + var getField = new GetFieldInstruction(); + getField.setField(new FieldReference("java.lang.String", "characters")); + getField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + getField.setInstance(invoke.getInstance()); + getField.setReceiver(program.createVariable()); + getField.setLocation(invoke.getLocation()); + invoke.insertNext(getField); + + var arrayCopy = new InvokeInstruction(); + arrayCopy.setType(InvocationType.SPECIAL); + arrayCopy.setMethod(new MethodReference(System.class, "arraycopy", Object.class, int.class, + Object.class, int.class, int.class, void.class)); + arrayCopy.setArguments(getField.getReceiver(), invoke.getArguments().get(0), invoke.getArguments().get(1), + invoke.getArguments().get(2), invoke.getArguments().get(3)); + getField.insertNext(arrayCopy); + + invoke.delete(); + } + + private void replaceFastCharArray(InvokeInstruction invoke) { + var getField = new GetFieldInstruction(); + getField.setField(new FieldReference("java.lang.String", "characters")); + getField.setFieldType(ValueType.arrayOf(ValueType.CHARACTER)); + getField.setInstance(invoke.getInstance()); + getField.setReceiver(invoke.getReceiver()); + getField.setLocation(invoke.getLocation()); + invoke.replace(getField); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringConstructorGenerator.java b/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringConstructorGenerator.java new file mode 100644 index 000000000..cdedfd357 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringConstructorGenerator.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.impl.string; + +import java.io.IOException; +import org.teavm.backend.javascript.codegen.SourceWriter; +import org.teavm.backend.javascript.spi.Generator; +import org.teavm.backend.javascript.spi.GeneratorContext; +import org.teavm.model.MethodReference; + +public class JSStringConstructorGenerator implements Generator { + @Override + public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) throws IOException { + writer.append(context.getParameterName(0)); + writer.append(".").appendField(JSStringInjector.NATIVE_FIELD).ws().append("=").ws(); + writer.append(context.getParameterName(1)).append(";").softNewLine(); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringInjector.java b/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringInjector.java new file mode 100644 index 000000000..74f7b9a43 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringInjector.java @@ -0,0 +1,234 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.impl.string; + +import java.io.IOException; +import java.util.function.Function; +import org.teavm.backend.javascript.ProviderContext; +import org.teavm.backend.javascript.spi.Injector; +import org.teavm.backend.javascript.spi.InjectorContext; +import org.teavm.model.FieldReference; +import org.teavm.model.MethodReference; + +public class JSStringInjector implements Injector, Function { + static final FieldReference NATIVE_FIELD = new FieldReference("java.lang.String", "nativeString"); + + @Override + public Injector apply(ProviderContext providerContext) { + switch (providerContext.getMethod().getName()) { + case "initWithEmptyChars": + case "borrowChars": + case "initWithCharArray": + case "takeCharArray": + case "charactersLength": + case "charactersGet": + case "copyCharsToArray": + case "fastCharArray": + case "nativeString": + case "substringJS": + case "toLowerCaseJS": + case "toUpperCaseJS": + case "intern": + case "stripJS": + case "stripLeadingJS": + case "stripTrailingJS": + return this; + } + return null; + } + + @Override + public void generate(InjectorContext context, MethodReference methodRef) throws IOException { + switch (methodRef.getName()) { + case "initWithEmptyChars": + initWithEmptyChars(context); + break; + case "borrowChars": + borrowChars(context); + break; + case "initWithCharArray": + initWithCharArray(context); + break; + case "takeCharArray": + takeCharArray(context); + break; + case "charactersLength": + charactersLength(context); + break; + case "charactersGet": + charactersGet(context); + break; + case "copyCharsToArray": + copyCharsToArray(context); + break; + case "fastCharArray": + fastCharArray(context); + break; + case "nativeString": + nativeString(context); + break; + case "substringJS": + substringJS(context); + break; + case "toLowerCaseJS": + toLowerCaseJS(context); + break; + case "toUpperCaseJS": + toUpperCaseJS(context); + break; + case "intern": + intern(context); + break; + case "stripJS": + stripJS(context); + break; + case "stripLeadingJS": + stripLeadingJS(context); + break; + case "stripTrailingJS": + stripTrailingJS(context); + break; + } + } + + private void initWithEmptyChars(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD).ws().append("=").ws().append("\"\""); + } + + private void borrowChars(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD).ws().append("=").ws(); + context.writeExpr(context.getArgument(1)); + writer.append(".").appendField(NATIVE_FIELD); + } + + private void initWithCharArray(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD).ws().append("=").ws(); + writer.append("$rt_charArrayToString").append("("); + context.writeExpr(context.getArgument(1)); + writer.append(".data,").ws(); + context.writeExpr(context.getArgument(2)); + writer.append(",").ws(); + context.writeExpr(context.getArgument(3)); + writer.append(")"); + } + + private void takeCharArray(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD).ws().append("=").ws(); + writer.append("$rt_fullArrayToString").append("("); + context.writeExpr(context.getArgument(1)); + writer.append(".data)"); + } + + private void charactersLength(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD).append(".length"); + } + + private void charactersGet(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD).append(".charCodeAt("); + context.writeExpr(context.getArgument(1)); + writer.append(")"); + } + + private void copyCharsToArray(InjectorContext context) throws IOException { + var writer = context.getWriter(); + writer.append("$rt_stringToCharArray").append("("); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD); + writer.append(",").ws(); + context.writeExpr(context.getArgument(1)); + writer.append(",").ws(); + context.writeExpr(context.getArgument(2)); + writer.append(".data"); + writer.append(",").ws(); + context.writeExpr(context.getArgument(3)); + writer.append(",").ws(); + context.writeExpr(context.getArgument(4)); + writer.append(")"); + } + + private void substringJS(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".substring("); + context.writeExpr(context.getArgument(1)); + writer.append(",").ws(); + context.writeExpr(context.getArgument(2)); + writer.append(")"); + } + + private void fastCharArray(InjectorContext context) throws IOException { + var writer = context.getWriter(); + writer.append("$rt_fastStringToCharArray").append("("); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD); + writer.append(")"); + } + + private void nativeString(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".").appendField(NATIVE_FIELD); + } + + private void toLowerCaseJS(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".toLowerCase()"); + } + + private void toUpperCaseJS(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".toUpperCase()"); + } + + private void intern(InjectorContext context) throws IOException { + var writer = context.getWriter(); + writer.appendFunction("$rt_intern").append("("); + context.writeExpr(context.getArgument(0)); + writer.append(")"); + } + + private void stripJS(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".trim()"); + } + + private void stripLeadingJS(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".trimStart()"); + } + + private void stripTrailingJS(InjectorContext context) throws IOException { + var writer = context.getWriter(); + context.writeExpr(context.getArgument(0)); + writer.append(".trimEnd()"); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringTransformer.java b/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringTransformer.java new file mode 100644 index 000000000..9359dc979 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/impl/string/JSStringTransformer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.impl.string; + +import org.teavm.interop.NoSideEffects; +import org.teavm.model.AccessLevel; +import org.teavm.model.AnnotationHolder; +import org.teavm.model.ClassHolder; +import org.teavm.model.ClassHolderTransformer; +import org.teavm.model.ClassHolderTransformerContext; +import org.teavm.model.ElementModifier; +import org.teavm.model.FieldHolder; +import org.teavm.model.MethodDescriptor; +import org.teavm.model.ValueType; + +public class JSStringTransformer implements ClassHolderTransformer { + @Override + public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) { + if (cls.getName().equals("java.lang.String")) { + var charactersField = new FieldHolder("nativeString"); + charactersField.setType(ValueType.object("java.lang.Object")); + charactersField.setLevel(AccessLevel.PRIVATE); + cls.addField(charactersField); + + var method = cls.getMethod(new MethodDescriptor("", Object.class, void.class)); + method.setProgram(null); + method.getModifiers().add(ElementModifier.NATIVE); + method.getAnnotations().add(new AnnotationHolder(NoSideEffects.class.getName())); + } + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/StringNativeGenerator.java b/classlib/src/main/java/org/teavm/classlib/java/lang/StringNativeDependency.java similarity index 71% rename from classlib/src/main/java/org/teavm/classlib/java/lang/StringNativeGenerator.java rename to classlib/src/main/java/org/teavm/classlib/java/lang/StringNativeDependency.java index 2ab178f25..6a2843446 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/StringNativeGenerator.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/StringNativeDependency.java @@ -15,23 +15,12 @@ */ package org.teavm.classlib.java.lang; -import java.io.IOException; -import org.teavm.backend.javascript.codegen.SourceWriter; -import org.teavm.backend.javascript.spi.Generator; -import org.teavm.backend.javascript.spi.GeneratorContext; import org.teavm.dependency.DependencyAgent; import org.teavm.dependency.DependencyPlugin; import org.teavm.dependency.MethodDependency; import org.teavm.model.MethodReference; -public class StringNativeGenerator implements Generator, DependencyPlugin { - @Override - public void generate(GeneratorContext context, SourceWriter writer, MethodReference methodRef) throws IOException { - if (methodRef.getName().equals("intern")) { - writer.append("return $rt_intern(").append(context.getParameterName(0)).append(");").softNewLine(); - } - } - +public class StringNativeDependency implements DependencyPlugin { @Override public void methodReached(DependencyAgent agent, MethodDependency method) { if (method.getReference().getName().equals("intern")) { diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TString.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TString.java index 69ae16c20..1c5b5e195 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/TString.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TString.java @@ -16,7 +16,7 @@ package org.teavm.classlib.java.lang; import java.util.Locale; -import org.teavm.backend.javascript.spi.GeneratedBy; +import org.teavm.classlib.PlatformDetector; import org.teavm.classlib.java.io.TSerializable; import org.teavm.classlib.java.io.TUnsupportedEncodingException; import org.teavm.classlib.java.nio.TByteBuffer; @@ -35,32 +35,50 @@ public class TString extends TObject implements TSerializable, TComparable CASE_INSENSITIVE_ORDER = (o1, o2) -> o1.compareToIgnoreCase(o2); - private char[] characters; private transient int hashCode; public TString() { - this.characters = EMPTY_CHARS; + initWithEmptyChars(); } + @NoSideEffects + private native void initWithEmptyChars(); + public TString(TString other) { - characters = other.characters; + borrowChars(other); } + @NoSideEffects + private native void borrowChars(TString other); + public TString(char[] characters) { this(characters, 0, characters.length); } - public TString(char[] value, int offset, int count) { - this.characters = new char[count]; - System.arraycopy(value, offset, this.characters, 0, count); + public TString(Object nativeString) { } + private native Object nativeString(); + + public TString(char[] value, int offset, int count) { + if (offset < 0 || count < 0 || offset + count > value.length) { + throw new IndexOutOfBoundsException(); + } + initWithCharArray(value, offset, count); + } + + @NoSideEffects + private native void initWithCharArray(char[] value, int offset, int count); + static TString fromArray(char[] characters) { var s = new TString(); - s.characters = characters; + s.takeCharArray(characters); return s; } + @NoSideEffects + private native void takeCharArray(char[] characters); + public TString(byte[] bytes, int offset, int length, TString charsetName) throws TUnsupportedEncodingException { this(bytes, offset, length, TCharset.forName(charsetName.toString())); } @@ -86,7 +104,7 @@ public class TString extends TObject implements TSerializable, TComparable= characters.length) { + if (index < 0 || index >= charactersLength()) { throw new TStringIndexOutOfBoundsException(); } - return characters[index]; + return charactersGet(index); } public int codePointAt(int index) { @@ -150,17 +171,23 @@ public class TString extends TObject implements TSerializable, TComparable dst.length) { throw new TIndexOutOfBoundsException(); } - System.arraycopy(characters, srcBegin, dst, dstBegin, srcEnd - srcBegin); + copyCharsToArray(srcBegin, dst, dstBegin, srcEnd - srcBegin); } + @NoSideEffects + private native void copyCharsToArray(int begin, char[] dst, int dstBegin, int length); + public boolean contentEquals(TStringBuffer buffer) { - if (characters.length != buffer.length()) { + if (charactersLength() != buffer.length()) { return false; } - for (int i = 0; i < characters.length; ++i) { - if (characters[i] != buffer.charAt(i)) { + for (int i = 0; i < charactersLength(); ++i) { + if (charactersGet(i) != buffer.charAt(i)) { return false; } } @@ -191,11 +221,11 @@ public class TString extends TObject implements TSerializable, TComparable= 0; --i) { - if (characters[i] == bmpChar) { + if (charactersGet(i) == bmpChar) { return i; } } @@ -338,7 +368,7 @@ public class TString extends TObject implements TSerializable, TComparable= 1; --i) { - if (characters[i] == lo && characters[i - 1] == hi) { + if (charactersGet(i) == lo && charactersGet(i - 1) == hi) { return i - 1; } } @@ -394,12 +424,21 @@ public class TString extends TObject implements TSerializable, TComparable", char[].class, void.class)); + dep = dependencyAnalyzer.linkMethod(new MethodReference(String.class, "", Object.class, void.class)); dep.getVariable(0).propagate(stringType); - dep.getVariable(1).propagate(dependencyAnalyzer.getType("[C")); dep.use(); dependencyAnalyzer.linkField(new FieldReference(String.class.getName(), "characters")); diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java b/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java index a2e91f66b..dd4e8c3df 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/Renderer.java @@ -263,7 +263,8 @@ public class Renderer implements RenderingManager { "$rt_createShortArray", "$rt_createCharArray", "$rt_createIntArray", "$rt_createLongArray", "$rt_createFloatArray", "$rt_createDoubleArray", "$rt_compare", "$rt_castToClass", "$rt_castToInterface", "$rt_equalDoubles", - "Long_toNumber", "Long_fromInt", "Long_fromNumber", "Long_create", "Long_ZERO", + "$rt_str", "Long_toNumber", "Long_fromInt", "Long_fromNumber", "Long_create", "Long_ZERO", + "$rt_intern", "$rt_substring", "Long_hi", "Long_lo"); } diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/RuntimeRenderer.java b/core/src/main/java/org/teavm/backend/javascript/rendering/RuntimeRenderer.java index 661fcad60..6ed87eec1 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/RuntimeRenderer.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/RuntimeRenderer.java @@ -73,6 +73,7 @@ public class RuntimeRenderer { renderRuntimeThrowableMethods(); renderRuntimeNullCheck(); renderRuntimeIntern(); + renderStringClassInit(); renderRuntimeThreads(); renderRuntimeCreateException(); renderCreateStackTraceElement(); @@ -123,33 +124,21 @@ public class RuntimeRenderer { } private void renderRuntimeString() throws IOException { - MethodReference stringCons = new MethodReference(String.class, "", char[].class, void.class); - writer.append("function $rt_str(str) {").indent().softNewLine(); + MethodReference stringCons = new MethodReference(String.class, "", Object.class, void.class); + writer.append("function $rt_str(str)").ws().append("{").indent().softNewLine(); writer.append("if (str === null) {").indent().softNewLine(); writer.append("return null;").softNewLine(); writer.outdent().append("}").softNewLine(); - writer.append("var characters = $rt_createCharArray(str.length);").softNewLine(); - writer.append("var charsBuffer = characters.data;").softNewLine(); - writer.append("for (var i = 0; i < str.length; i = (i + 1) | 0) {").indent().softNewLine(); - writer.append("charsBuffer[i] = str.charCodeAt(i) & 0xFFFF;").softNewLine(); - writer.outdent().append("}").softNewLine(); - writer.append("return ").appendInit(stringCons).append("(characters);").softNewLine(); + writer.append("return ").appendInit(stringCons).append("(str);").softNewLine(); writer.outdent().append("}").newLine(); } private void renderRuntimeUnwrapString() throws IOException { - FieldReference stringChars = new FieldReference(STRING_CLASS, "characters"); - writer.append("function $rt_ustr(str) {").indent().softNewLine(); - writer.append("if (str === null) {").indent().softNewLine(); - writer.append("return null;").softNewLine(); - writer.outdent().append("}").softNewLine(); - - writer.append("var data = str.").appendField(stringChars).append(".data;").softNewLine(); - writer.append("var result = \"\";").softNewLine(); - writer.append("for (var i = 0; i < data.length; i = (i + 1) | 0) {").indent().softNewLine(); - writer.append("result += String.fromCharCode(data[i]);").softNewLine(); - writer.outdent().append("}").softNewLine(); - writer.append("return result;").softNewLine(); + FieldReference stringChars = new FieldReference(STRING_CLASS, "nativeString"); + writer.append("function $rt_ustr(str)").ws().append("{").indent().softNewLine(); + writer.append("return str").ws().append("!==").ws().append("null"); + writer.ws().append("?").ws().append("str.").appendField(stringChars); + writer.ws().append(":").ws().append("null").append(";").softNewLine(); writer.outdent().append("}").newLine(); } @@ -169,17 +158,15 @@ public class RuntimeRenderer { writer.outdent().append("}").softNewLine(); } else { renderHandWrittenRuntime("intern.js"); - writer.append("function $rt_stringHash(s)").ws().append("{").indent().softNewLine(); - writer.append("return ").appendMethodBody(String.class, "hashCode", int.class) - .append("(s);").softNewLine(); - writer.outdent().append("}").softNewLine(); - writer.append("function $rt_stringEquals(a,").ws().append("b)").ws().append("{").indent().softNewLine(); - writer.append("return ").appendMethodBody(String.class, "equals", Object.class, boolean.class) - .append("(a").ws().append(",b);").softNewLine(); - writer.outdent().append("}").softNewLine(); } } + private void renderStringClassInit() throws IOException { + writer.append("function $rt_stringClassInit(str)").ws().append("{").indent().softNewLine(); + writer.appendClassInit("java.lang.String").append("();").softNewLine(); + writer.outdent().append("}").softNewLine(); + } + private boolean needInternMethod() { ClassReader cls = classSource.get(STRING_CLASS); if (cls == null) { diff --git a/core/src/main/resources/org/teavm/backend/javascript/intern.js b/core/src/main/resources/org/teavm/backend/javascript/intern.js index dae99429d..9805be5ce 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/intern.js +++ b/core/src/main/resources/org/teavm/backend/javascript/intern.js @@ -16,49 +16,34 @@ "use strict"; var $rt_intern = function() { - var table = new Array(100); - var size = 0; + var map = Object.create(null); - function get(str) { - var hash = $rt_stringHash(str); - var bucket = getBucket(hash); - for (var i = 0; i < bucket.length; ++i) { - if ($rt_stringEquals(bucket[i], str)) { - return bucket[i]; + var get; + if (typeof WeakRef !== 'undefined') { + var registry = new FinalizationRegistry(value => { + delete map[value]; + }); + + get = function(str) { + var key = $rt_ustr(str); + var ref = map[key]; + var result = typeof ref !== 'undefined' ? ref.deref() : void 0; + if (typeof result !== 'object') { + result = str; + map[key] = new WeakRef(result); + registry.register(result, key); } + return result; } - bucket.push(str); - return str; - } - - function getBucket(hash) { - while (true) { - var position = hash % table.length; - var bucket = table[position]; - if (typeof bucket !== "undefined") { - return bucket; - } - if (++size / table.length > 0.5) { - rehash(); - } else { - bucket = []; - table[position] = bucket; - return bucket; - } - } - } - - function rehash() { - var old = table; - table = new Array(table.length * 2); - size = 0; - for (var i = 0; i < old.length; ++i) { - var bucket = old[i]; - if (typeof bucket !== "undefined") { - for (var j = 0; j < bucket.length; ++j) { - get(bucket[j]); - } + } else { + get = function(str) { + var key = $rt_ustr(str); + var result = map[key]; + if (typeof result !== 'object') { + result = str; + map[key] = result; } + return result; } } diff --git a/core/src/main/resources/org/teavm/backend/javascript/runtime.js b/core/src/main/resources/org/teavm/backend/javascript/runtime.js index 8643f4eb2..c1bede99b 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/runtime.js +++ b/core/src/main/resources/org/teavm/backend/javascript/runtime.js @@ -644,6 +644,7 @@ function $rt_mainStarter(f) { } var $rt_stringPool_instance; function $rt_stringPool(strings) { + $rt_stringClassInit(); $rt_stringPool_instance = new Array(strings.length); for (var i = 0; i < strings.length; ++i) { $rt_stringPool_instance[i] = $rt_intern($rt_str(strings[i])); @@ -937,4 +938,36 @@ function $rt_classWithoutFields(superclass) { return function() { superclass.call(this); }; -} \ No newline at end of file +} +function $rt_charArrayToString(array, offset, count) { + var result = ""; + var limit = offset + count; + for (var i = offset; i < limit; i = (i + 1024) | 0) { + var next = Math.min(limit, (i + 1024) | 0); + result += String.fromCharCode.apply(null, array.subarray(i, next)); + } + return result; +} +function $rt_fullArrayToString(array) { + return $rt_charArrayToString(array, 0, array.length); +} +function $rt_stringToCharArray(string, begin, dst, dstBegin, count) { + for (var i = 0; i < count; i = (i + 1) | 0) { + dst[dstBegin + i] = string.charCodeAt(begin + i); + } +} +function $rt_fastStringToCharArray(string) { + var array = new Uint16Array(string.length); + for (var i = 0; i < array.length; ++i) { + array[i] = string.charCodeAt(i); + } + return $rt_createNumericArray($rt_charcls(), array); +} +function $rt_substring(string, start, end) { + if (start === 0 && end === string.length) { + return string; + } + var result = start.substring(start, end - 1) + start.substring(end - 1, end); + $rt_substringSink = ($rt_substringSink + result.charCodeAt(result.length - 1)) | 0; +} +var $rt_substringSink = 0; \ No newline at end of file diff --git a/html4j/src/main/java/org/teavm/html4j/JavaScriptConvGenerator.java b/html4j/src/main/java/org/teavm/html4j/JavaScriptConvGenerator.java index a415a1b0f..4f8455f28 100644 --- a/html4j/src/main/java/org/teavm/html4j/JavaScriptConvGenerator.java +++ b/html4j/src/main/java/org/teavm/html4j/JavaScriptConvGenerator.java @@ -125,7 +125,7 @@ public class JavaScriptConvGenerator implements Generator { writer.outdent().append("} else if (" + type + " === ").appendClass("java.lang.String") .append(") {").indent().softNewLine(); - writer.append("return $rt_str(" + obj + ");").softNewLine(); + writer.append("return ").appendFunction("$rt_str").append("(").append(obj).append(");").softNewLine(); writer.outdent().append("} else if (" + type + " === ").appendClass("java.lang.Boolean") .append(") {").indent().softNewLine(); @@ -179,7 +179,7 @@ public class JavaScriptConvGenerator implements Generator { writer.append("return arr;").softNewLine(); writer.outdent().append("} else if (typeof " + obj + " === 'string') {").indent().softNewLine(); - writer.append("return $rt_str(" + obj + ");").softNewLine(); + writer.append("return ").appendFunction("$rt_str").append("(" + obj + ");").softNewLine(); writer.outdent().append("} else if (typeof " + obj + " === 'number') {").indent().softNewLine(); writer.append("if ((" + obj + "|0) === " + obj + ") {").indent().softNewLine(); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeGenerator.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeGenerator.java index 947ea27c8..273682571 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeGenerator.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeGenerator.java @@ -166,7 +166,7 @@ public class JSNativeGenerator implements Injector, DependencyPlugin, Generator } break; case "unwrapString": - writer.append("$rt_str("); + writer.appendFunction("$rt_str").append("("); context.writeExpr(context.getArgument(0), Precedence.min()); writer.append(")"); break; diff --git a/platform/src/main/java/org/teavm/platform/plugin/ResourceAccessorInjector.java b/platform/src/main/java/org/teavm/platform/plugin/ResourceAccessorInjector.java index f91115782..55849881f 100644 --- a/platform/src/main/java/org/teavm/platform/plugin/ResourceAccessorInjector.java +++ b/platform/src/main/java/org/teavm/platform/plugin/ResourceAccessorInjector.java @@ -87,7 +87,7 @@ class ResourceAccessorInjector implements Injector { context.getWriter().append('('); context.writeExpr(context.getArgument(0)); context.getWriter().ws().append("!==").ws().append("null").ws().append("?").ws(); - context.getWriter().append("$rt_str("); + context.getWriter().appendFunction("$rt_str").append("("); context.writeExpr(context.getArgument(0)); context.getWriter().append(")").ws().append(':').ws().append("null)"); break; diff --git a/tests/src/test/java/org/teavm/classlib/java/lang/StringTest.java b/tests/src/test/java/org/teavm/classlib/java/lang/StringTest.java index 04c231dc3..60212d17d 100644 --- a/tests/src/test/java/org/teavm/classlib/java/lang/StringTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/lang/StringTest.java @@ -354,5 +354,5 @@ public class StringTest { assertTrue(new String(new char[] { ' ', ' ' }).isBlank()); assertFalse(new String(new char[] { ' ', 'x', ' ' }).isBlank()); assertFalse(new String(new char[] { 'a', ' ' }).isBlank()); - } + } } diff --git a/tests/src/test/java/org/teavm/incremental/EntryPointGenerator.java b/tests/src/test/java/org/teavm/incremental/EntryPointGenerator.java index baa74b362..96c6c200e 100644 --- a/tests/src/test/java/org/teavm/incremental/EntryPointGenerator.java +++ b/tests/src/test/java/org/teavm/incremental/EntryPointGenerator.java @@ -26,7 +26,6 @@ public class EntryPointGenerator implements Injector { public void generate(InjectorContext context, MethodReference methodRef) throws IOException { context.getWriter().append("main.result = ("); context.writeExpr(context.getArgument(0)); - context.getWriter().append(").").appendField(new FieldReference("java.lang.String", "characters")); - context.getWriter().append(".data"); + context.getWriter().append(").").appendField(new FieldReference("java.lang.String", "nativeString")); } } diff --git a/tests/src/test/java/org/teavm/incremental/IncrementalTest.java b/tests/src/test/java/org/teavm/incremental/IncrementalTest.java index e0ca53752..d1a177ea2 100644 --- a/tests/src/test/java/org/teavm/incremental/IncrementalTest.java +++ b/tests/src/test/java/org/teavm/incremental/IncrementalTest.java @@ -40,7 +40,6 @@ import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; -import org.mozilla.javascript.typedarrays.NativeUint16Array; import org.teavm.ast.AsyncMethodNode; import org.teavm.backend.javascript.JavaScriptTarget; import org.teavm.cache.AlwaysStaleCacheStatus; @@ -169,12 +168,7 @@ public class IncrementalTest { Function main = (Function) scope.get("main", scope); ScriptRuntime.doTopCall(main, rhinoContext, scope, scope, new Object[] { new NativeArray(0), Undefined.instance }); - NativeUint16Array jsChars = (NativeUint16Array) main.get("result", main); - char[] chars = new char[jsChars.getArrayLength()]; - for (int i = 0; i < chars.length; ++i) { - chars[i] = (char) jsChars.get(i).intValue(); - } - return new String(chars); + return main.get("result", main).toString(); } private static String getSimpleName(String name) {