js: add JSTopLevel annotation that allows to import top-level declarations

This commit is contained in:
Alexey Andreev 2024-04-11 20:29:57 +02:00
parent 9b41e3e814
commit 6a09f181c7
16 changed files with 290 additions and 46 deletions

View File

@ -109,3 +109,5 @@ let $rt_setThread = t => {
} }
let $rt_apply = (instance, method, args) => instance[method].apply(instance, args); let $rt_apply = (instance, method, args) => instance[method].apply(instance, args);
let $rt_apply_topLevel = (method, args) => method.apply(null, args);

View File

@ -15,9 +15,9 @@
*/ */
package org.teavm.jso.browser; package org.teavm.jso.browser;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty; import org.teavm.jso.JSProperty;
import org.teavm.jso.JSTopLevel;
import org.teavm.jso.core.JSArray; import org.teavm.jso.core.JSArray;
import org.teavm.jso.core.JSArrayReader; import org.teavm.jso.core.JSArrayReader;
import org.teavm.jso.dom.html.HTMLDocument; import org.teavm.jso.dom.html.HTMLDocument;
@ -81,47 +81,47 @@ public abstract class Window implements JSObject, WindowEventTarget, StorageProv
@JSProperty @JSProperty
public abstract Window getTop(); public abstract Window getTop();
@JSBody(params = "message", script = "alert(message);") @JSTopLevel
public static native void alert(JSObject message); public static native void alert(JSObject message);
@JSBody(params = "message", script = "alert(message);") @JSTopLevel
public static native void alert(String message); public static native void alert(String message);
@JSBody(params = "message", script = "return confirm(message);") @JSTopLevel
public static native boolean confirm(JSObject message); public static native boolean confirm(JSObject message);
@JSBody(params = "message", script = "return confirm(message);") @JSTopLevel
public static native boolean confirm(String message); public static native boolean confirm(String message);
public static String prompt(String message) { public static String prompt(String message) {
return prompt(message, ""); return prompt(message, "");
} }
@JSBody(params = { "message", "defaultValue" }, script = "return prompt(message, defaultValue);") @JSTopLevel
public static native String prompt(String message, String defaultValue); public static native String prompt(String message, String defaultValue);
@JSBody(params = { "handler", "delay" }, script = "return setTimeout(handler, delay);") @JSTopLevel
public static native int setTimeout(TimerHandler handler, int delay); public static native int setTimeout(TimerHandler handler, int delay);
@JSBody(params = { "handler", "delay" }, script = "return setTimeout(handler, delay);") @JSTopLevel
public static native int setTimeout(TimerHandler handler, double delay); public static native int setTimeout(TimerHandler handler, double delay);
@JSBody(params = "timeoutId", script = "clearTimeout(timeoutId);") @JSTopLevel
public static native void clearTimeout(int timeoutId); public static native void clearTimeout(int timeoutId);
@JSBody(params = { "handler", "delay" }, script = "return setInterval(handler, delay);") @JSTopLevel
public static native int setInterval(TimerHandler handler, int delay); public static native int setInterval(TimerHandler handler, int delay);
@JSBody(params = { "handler", "delay" }, script = "return setInterval(handler, delay);") @JSTopLevel
public static native int setInterval(TimerHandler handler, double delay); public static native int setInterval(TimerHandler handler, double delay);
@JSBody(params = "timeoutId", script = "clearInterval(timeoutId);") @JSTopLevel
public static native void clearInterval(int timeoutId); public static native void clearInterval(int timeoutId);
@JSBody(params = "callback", script = "return requestAnimationFrame(callback);") @JSTopLevel
public static native int requestAnimationFrame(AnimationFrameCallback callback); public static native int requestAnimationFrame(AnimationFrameCallback callback);
@JSBody(params = "requestId", script = "cancelAnimationFrame(requestId);") @JSTopLevel
public static native void cancelAnimationFrame(int requestId); public static native void cancelAnimationFrame(int requestId);
public abstract void blur(); public abstract void blur();
@ -170,30 +170,32 @@ public abstract class Window implements JSObject, WindowEventTarget, StorageProv
postMessage(message, JSArray.of(transfer)); postMessage(message, JSArray.of(transfer));
} }
@JSBody(script = "return window;") @JSTopLevel
@JSProperty("window")
public static native Window current(); public static native Window current();
@JSBody(script = "return self;") @JSTopLevel
@JSProperty("self")
public static native Window worker(); public static native Window worker();
@JSBody(params = "uri", script = "return encodeURI(uri);") @JSTopLevel
public static native String encodeURI(String uri); public static native String encodeURI(String uri);
@JSBody(params = "uri", script = "return encodeURIComponent(uri);") @JSTopLevel
public static native String encodeURIComponent(String uri); public static native String encodeURIComponent(String uri);
@JSBody(params = "uri", script = "return decodeURI(uri);") @JSTopLevel
public static native String decodeURI(String uri); public static native String decodeURI(String uri);
@JSBody(params = "uri", script = "return decodeURIComponent(uri);") @JSTopLevel
public static native String decodeURIComponent(String uri); public static native String decodeURIComponent(String uri);
@JSProperty @JSProperty
public abstract double getDevicePixelRatio(); public abstract double getDevicePixelRatio();
@JSBody(params = "s", script = "return atob(s);") @JSTopLevel
public static native String atob(String s); public static native String atob(String s);
@JSBody(params = "s", script = "return btoa(s);") @JSTopLevel
public static native String btoa(String s); public static native String btoa(String s);
} }

View File

@ -21,7 +21,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target({ ElementType.TYPE, ElementType.METHOD })
public @interface JSModule { public @interface JSModule {
String value(); String value();
} }

View File

@ -0,0 +1,26 @@
/*
* Copyright 2024 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.jso;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface JSTopLevel {
}

View File

@ -39,6 +39,7 @@ import org.teavm.jso.JSClass;
import org.teavm.jso.JSFunctor; import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSPrimitiveType; import org.teavm.jso.JSPrimitiveType;
import org.teavm.jso.JSTopLevel;
import org.teavm.model.AnnotationContainerReader; import org.teavm.model.AnnotationContainerReader;
import org.teavm.model.AnnotationHolder; import org.teavm.model.AnnotationHolder;
import org.teavm.model.AnnotationReader; import org.teavm.model.AnnotationReader;
@ -991,9 +992,19 @@ class JSClassProcessor {
} }
private Variable getCallTarget(InvokeInstruction invoke) { private Variable getCallTarget(InvokeInstruction invoke) {
return invoke.getInstance() != null if (invoke.getInstance() != null) {
? invoke.getInstance() return invoke.getInstance();
: marshaller.classRef(invoke.getMethod().getClassName(), invoke.getLocation()); }
var cls = classSource.get(invoke.getMethod().getClassName());
var method = cls != null ? cls.getMethod(invoke.getMethod().getDescriptor()) : null;
var isTopLevel = (cls != null && cls.getAnnotations().get(JSTopLevel.class.getName()) != null)
|| (method != null && method.getAnnotations().get(JSTopLevel.class.getName()) != null);
if (isTopLevel) {
var methodAnnotations = method != null ? method.getAnnotations() : null;
return marshaller.moduleRef(invoke.getMethod().getClassName(), methodAnnotations, invoke.getLocation());
} else {
return marshaller.classRef(invoke.getMethod().getClassName(), invoke.getLocation());
}
} }
private boolean processConstructor(MethodReader method, CallLocation callLocation, InvokeInstruction invoke) { private boolean processConstructor(MethodReader method, CallLocation callLocation, InvokeInstruction invoke) {

View File

@ -63,19 +63,31 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
break; break;
case "get": case "get":
case "getPure": case "getPure":
if (isNull(context.getArgument(0))) {
writer.append(extractPropertyName(context.getArgument(1)));
} else {
context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS); context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS);
renderProperty(context.getArgument(1), context); renderProperty(context.getArgument(1), context);
}
break; break;
case "set": case "set":
case "setPure": case "setPure":
context.writeExpr(context.getArgument(0), Precedence.ASSIGNMENT.next()); if (isNull(context.getArgument(0))) {
writer.append(extractPropertyName(context.getArgument(1)));
} else {
context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS.next());
renderProperty(context.getArgument(1), context); renderProperty(context.getArgument(1), context);
}
writer.ws().append('=').ws(); writer.ws().append('=').ws();
context.writeExpr(context.getArgument(2), Precedence.ASSIGNMENT.next()); context.writeExpr(context.getArgument(2), Precedence.ASSIGNMENT.next());
break; break;
case "invoke": case "invoke":
if (isNull(context.getArgument(0))) {
writer.append(extractPropertyName(context.getArgument(1)));
} else {
context.writeExpr(context.getArgument(0), Precedence.GROUPING); context.writeExpr(context.getArgument(0), Precedence.GROUPING);
renderProperty(context.getArgument(1), context); renderProperty(context.getArgument(1), context);
}
writer.append('('); writer.append('(');
for (int i = 2; i < context.argumentCount(); ++i) { for (int i = 2; i < context.argumentCount(); ++i) {
if (i > 2) { if (i > 2) {
@ -234,15 +246,30 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
} }
} }
private static boolean isNull(Expr expr) {
if (expr instanceof ConstantExpr) {
var constantExpr = (ConstantExpr) expr;
if (constantExpr.getValue() == null) {
return true;
}
}
return false;
}
private void applyFunction(InjectorContext context) { private void applyFunction(InjectorContext context) {
if (tryApplyFunctionOptimized(context)) { if (tryApplyFunctionOptimized(context)) {
return; return;
} }
var writer = context.getWriter(); var writer = context.getWriter();
if (isNull(context.getArgument(0))) {
writer.appendFunction("$rt_apply_topLevel").append("(");
writer.append(extractPropertyName(context.getArgument(1)));
} else {
writer.appendFunction("$rt_apply").append("("); writer.appendFunction("$rt_apply").append("(");
context.writeExpr(context.getArgument(0), Precedence.ASSIGNMENT); context.writeExpr(context.getArgument(0), Precedence.ASSIGNMENT);
writer.append(",").ws(); writer.append(",").ws();
context.writeExpr(context.getArgument(1), Precedence.ASSIGNMENT); context.writeExpr(context.getArgument(1), Precedence.ASSIGNMENT);
}
writer.append(",").ws(); writer.append(",").ws();
context.writeExpr(context.getArgument(2), Precedence.ASSIGNMENT); context.writeExpr(context.getArgument(2), Precedence.ASSIGNMENT);
writer.append(")"); writer.append(")");
@ -323,8 +350,12 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
private void applyFunctionOptimized(InjectorContext context, List<Expr> paramList) { private void applyFunctionOptimized(InjectorContext context, List<Expr> paramList) {
var writer = context.getWriter(); var writer = context.getWriter();
if (isNull(context.getArgument(0))) {
writer.append(extractPropertyName(context.getArgument(1)));
} else {
context.writeExpr(context.getArgument(0), Precedence.GROUPING); context.writeExpr(context.getArgument(0), Precedence.GROUPING);
renderProperty(context.getArgument(1), context); renderProperty(context.getArgument(1), context);
}
writer.append('('); writer.append('(');
for (int i = 0; i < paramList.size(); ++i) { for (int i = 0; i < paramList.size(); ++i) {
if (i > 0) { if (i > 0) {

View File

@ -22,6 +22,7 @@ import org.teavm.jso.JSClass;
import org.teavm.jso.JSFunctor; import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSModule; import org.teavm.jso.JSModule;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.model.AnnotationContainerReader;
import org.teavm.model.CallLocation; import org.teavm.model.CallLocation;
import org.teavm.model.ClassReader; import org.teavm.model.ClassReader;
import org.teavm.model.ClassReaderSource; import org.teavm.model.ClassReaderSource;
@ -36,6 +37,7 @@ import org.teavm.model.Variable;
import org.teavm.model.instructions.ClassConstantInstruction; import org.teavm.model.instructions.ClassConstantInstruction;
import org.teavm.model.instructions.InvocationType; import org.teavm.model.instructions.InvocationType;
import org.teavm.model.instructions.InvokeInstruction; import org.teavm.model.instructions.InvokeInstruction;
import org.teavm.model.instructions.NullConstantInstruction;
import org.teavm.model.instructions.StringConstantInstruction; import org.teavm.model.instructions.StringConstantInstruction;
class JSValueMarshaller { class JSValueMarshaller {
@ -563,6 +565,10 @@ class JSValueMarshaller {
} }
Variable classRef(String className, TextLocation location) { Variable classRef(String className, TextLocation location) {
return classRef(className, null, location);
}
Variable classRef(String className, AnnotationContainerReader annotations, TextLocation location) {
String name = null; String name = null;
String module = null; String module = null;
var cls = classSource.get(className); var cls = classSource.get(className);
@ -578,17 +584,37 @@ class JSValueMarshaller {
} }
} }
} }
var jsModule = cls.getAnnotations().get(JSModule.class.getName()); module = moduleName(cls.getAnnotations());
if (jsModule != null) {
module = jsModule.getValue("value").getString();
}
} }
if (name == null) { if (name == null) {
name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1); name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1);
} }
if (module == null && annotations != null) {
module = moduleName(annotations);
}
return module != null ? moduleRef(module, name, location) : globalRef(name, location); return module != null ? moduleRef(module, name, location) : globalRef(name, location);
} }
Variable moduleRef(String className, AnnotationContainerReader annotations, TextLocation location) {
String module = null;
var cls = classSource.get(className);
if (cls != null) {
module = moduleName(cls.getAnnotations());
}
if (module == null && annotations != null) {
module = moduleName(annotations);
}
return module != null ? moduleRef(module, location) : nullInstance(location);
}
private String moduleName(AnnotationContainerReader annotations) {
var jsModule = annotations.get(JSModule.class.getName());
if (jsModule != null) {
return jsModule.getValue("value").getString();
}
return null;
}
Variable globalRef(String name, TextLocation location) { Variable globalRef(String name, TextLocation location) {
var invoke = new InvokeInstruction(); var invoke = new InvokeInstruction();
invoke.setType(InvocationType.SPECIAL); invoke.setType(InvocationType.SPECIAL);
@ -602,6 +628,10 @@ class JSValueMarshaller {
} }
Variable moduleRef(String module, String name, TextLocation location) { Variable moduleRef(String module, String name, TextLocation location) {
return dot(moduleRef(module, location), name, location);
}
Variable moduleRef(String module, TextLocation location) {
var moduleNameInsn = new StringConstantInstruction(); var moduleNameInsn = new StringConstantInstruction();
moduleNameInsn.setReceiver(program.createVariable()); moduleNameInsn.setReceiver(program.createVariable());
moduleNameInsn.setConstant(module); moduleNameInsn.setConstant(module);
@ -616,14 +646,26 @@ class JSValueMarshaller {
invoke.setLocation(location); invoke.setLocation(location);
replacement.add(invoke); replacement.add(invoke);
return invoke.getReceiver();
}
Variable dot(Variable instance, String name, TextLocation location) {
var get = new InvokeInstruction(); var get = new InvokeInstruction();
get.setType(InvocationType.SPECIAL); get.setType(InvocationType.SPECIAL);
get.setMethod(JSMethods.GET_PURE); get.setMethod(JSMethods.GET_PURE);
get.setReceiver(program.createVariable()); get.setReceiver(program.createVariable());
get.setArguments(invoke.getReceiver(), addJsString(name, location)); get.setArguments(instance, addJsString(name, location));
get.setLocation(location); get.setLocation(location);
replacement.add(get); replacement.add(get);
return get.getReceiver(); return get.getReceiver();
} }
Variable nullInstance(TextLocation location) {
var nullConstant = new NullConstantInstruction();
nullConstant.setReceiver(program.createVariable());
nullConstant.setLocation(location);
replacement.add(nullConstant);
return nullConstant.getReceiver();
}
} }

View File

@ -20,6 +20,7 @@ import org.junit.runner.RunWith;
import org.teavm.jso.JSClass; import org.teavm.jso.JSClass;
import org.teavm.jso.JSMethod; import org.teavm.jso.JSMethod;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSTopLevel;
import org.teavm.junit.AttachJavaScript; import org.teavm.junit.AttachJavaScript;
import org.teavm.junit.EachTestCompiledSeparately; import org.teavm.junit.EachTestCompiledSeparately;
import org.teavm.junit.OnlyPlatform; import org.teavm.junit.OnlyPlatform;
@ -77,6 +78,28 @@ public class CallTest {
assertEquals("a:23,b:q,va:6:7:8", TestClass.restVararg(23, "q", intArray)); assertEquals("a:23,b:q,va:6:7:8", TestClass.restVararg(23, "q", intArray));
} }
@Test
@AttachJavaScript("org/teavm/jso/test/vararg.js")
public void topLevelVararg() {
assertEquals("tva:q:w", TestClass.topLevelVararg("q", "w"));
assertEquals("tva:23:42", TestClass.topLevelVarargInt(23, 42));
var array = new String[3];
for (var i = 0; i < array.length; ++i) {
array[i] = String.valueOf((char) ('A' + i));
}
assertEquals("tva:A:B:C", TestClass.topLevelVararg(array));
var intArray = new int[3];
for (var i = 0; i < array.length; ++i) {
intArray[i] = 6 + i;
}
assertEquals("tva:6:7:8", TestClass.topLevelVarargInt(intArray));
assertEquals("tva", TestClass.topLevelVararg());
assertEquals("tva", TestClass.topLevelVarargInt());
}
@JSClass @JSClass
public static class TestClass implements JSObject { public static class TestClass implements JSObject {
public static native String allVararg(String... args); public static native String allVararg(String... args);
@ -87,5 +110,12 @@ public class CallTest {
public static native String restVararg(String a, int b, String... args); public static native String restVararg(String a, int b, String... args);
public static native String restVararg(int a, String b, int... args); public static native String restVararg(int a, String b, int... args);
@JSTopLevel
public static native String topLevelVararg(String... args);
@JSTopLevel
@JSMethod("topLevelVararg")
public static native String topLevelVarargInt(int... args);
} }
} }

View File

@ -18,6 +18,7 @@ package org.teavm.jso.test;
import org.teavm.jso.JSClass; import org.teavm.jso.JSClass;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty; import org.teavm.jso.JSProperty;
import org.teavm.jso.JSTopLevel;
@JSClass @JSClass
public class ClassWithConstructor implements JSObject { public class ClassWithConstructor implements JSObject {
@ -33,4 +34,15 @@ public class ClassWithConstructor implements JSObject {
public native String bar(); public native String bar();
public static native String staticMethod(); public static native String staticMethod();
@JSTopLevel
public static native String topLevelFunction();
@JSTopLevel
@JSProperty
public static native String getTopLevelProperty();
@JSTopLevel
@JSProperty
public static native void setTopLevelProperty(String value);
} }

View File

@ -19,6 +19,7 @@ import org.teavm.jso.JSClass;
import org.teavm.jso.JSModule; import org.teavm.jso.JSModule;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty; import org.teavm.jso.JSProperty;
import org.teavm.jso.JSTopLevel;
@JSClass(name = "ClassWithConstructor") @JSClass(name = "ClassWithConstructor")
@JSModule("./testModule.js") @JSModule("./testModule.js")
@ -33,4 +34,11 @@ public class ClassWithConstructorInModule implements JSObject {
public native int getFoo(); public native int getFoo();
public native String bar(); public native String bar();
@JSTopLevel
public static native String topLevelFunction();
@JSTopLevel
@JSProperty
public static native String getTopLevelProperty();
} }

View File

@ -59,6 +59,22 @@ public class ImportClassTest {
assertEquals("static method called", ClassWithConstructor.staticMethod()); assertEquals("static method called", ClassWithConstructor.staticMethod());
} }
@Test
@AttachJavaScript("org/teavm/jso/test/classWithConstructor.js")
public void topLevel() {
assertEquals("top level", ClassWithConstructor.topLevelFunction());
assertEquals("top level prop", ClassWithConstructor.getTopLevelProperty());
ClassWithConstructor.setTopLevelProperty("update");
assertEquals("update", ClassWithConstructor.getTopLevelProperty());
assertEquals("top level", TopLevelDeclarations.topLevelFunction());
assertEquals("update", TopLevelDeclarations.getTopLevelProperty());
TopLevelDeclarations.setTopLevelProperty("update2");
assertEquals("update2", ClassWithConstructor.getTopLevelProperty());
}
@JSBody(script = "return {};") @JSBody(script = "return {};")
private static native O create(); private static native O create();

View File

@ -68,6 +68,14 @@ public class ImportModuleTest {
assertEquals(23, o.getFoo()); assertEquals(23, o.getFoo());
} }
@Test
@JsModuleTest
@ServeJS(from = "org/teavm/jso/test/classWithConstructorInModule.js", as = "testModule.js")
public void topLevel() {
assertEquals("top level", ClassWithConstructorInModule.topLevelFunction());
assertEquals("top level prop", ClassWithConstructorInModule.getTopLevelProperty());
}
@JSBody( @JSBody(
script = "return testModule.foo();", script = "return testModule.foo();",
imports = @JSBodyImport(alias = "testModule", fromModule = "./testModule.js") imports = @JSBodyImport(alias = "testModule", fromModule = "./testModule.js")

View File

@ -0,0 +1,36 @@
/*
* Copyright 2024 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.jso.test;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.JSTopLevel;
@JSClass
@JSTopLevel
public class TopLevelDeclarations implements JSObject {
private TopLevelDeclarations() {
}
public static native String topLevelFunction();
@JSProperty
public static native String getTopLevelProperty();
@JSProperty
public static native void setTopLevelProperty(String value);
}

View File

@ -31,3 +31,9 @@ class ClassWithConstructor {
return "static method called"; return "static method called";
} }
} }
function topLevelFunction() {
return "top level";
}
let topLevelProperty = "top level prop";

View File

@ -27,3 +27,9 @@ export class ClassWithConstructor {
return "bar called"; return "bar called";
} }
} }
export function topLevelFunction() {
return "top level";
}
export let topLevelProperty = "top level prop";

View File

@ -31,3 +31,11 @@ class TestClass {
return result; return result;
} }
} }
function topLevelVararg(...args) {
let result = "tva";
for (const arg of args) {
result += ":" + arg;
}
return result;
}