Tests for JVM back-end support of tail-call optimization
This commit is contained in:
committed by
Andrey Breslav
parent
9da3d2f051
commit
fb0ec573e0
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2010-2013 JetBrains s.r.o.
|
||||
*
|
||||
* 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.jetbrains.jet.checkers;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.intellij.openapi.util.io.FileUtil;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.jet.ConfigurationKind;
|
||||
import org.jetbrains.jet.cli.jvm.compiler.CliLightClassGenerationSupport;
|
||||
import org.jetbrains.jet.cli.jvm.compiler.JetCoreEnvironment;
|
||||
import org.jetbrains.jet.lang.resolve.calls.TailRecursionKind;
|
||||
import org.jetbrains.jet.lang.descriptors.CallableDescriptor;
|
||||
import org.jetbrains.jet.lang.descriptors.SimpleFunctionDescriptor;
|
||||
import org.jetbrains.jet.lang.psi.*;
|
||||
import org.jetbrains.jet.lang.resolve.AnalyzerScriptParameter;
|
||||
import org.jetbrains.jet.lang.resolve.BindingContext;
|
||||
import org.jetbrains.jet.lang.resolve.BindingTrace;
|
||||
import org.jetbrains.jet.lang.resolve.calls.model.ResolvedCall;
|
||||
import org.jetbrains.jet.lang.resolve.java.AnalyzerFacadeForJVM;
|
||||
import org.jetbrains.jet.lang.resolve.lazy.KotlinTestWithEnvironment;
|
||||
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AbstractTailRecursionTest extends KotlinTestWithEnvironment {
|
||||
|
||||
@Override
|
||||
protected JetCoreEnvironment createEnvironment() {
|
||||
return createEnvironmentWithMockJdk(ConfigurationKind.JDK_AND_ANNOTATIONS);
|
||||
}
|
||||
|
||||
public void doTest(@NotNull String testFile) throws IOException {
|
||||
JetFile file = JetPsiFactory.createFile(getProject(), FileUtil.loadFile(new File(testFile), true));
|
||||
List<JetFile> files = new ArrayList<JetFile>(Collections.singleton(file));
|
||||
|
||||
final BindingTrace trace = CliLightClassGenerationSupport.getInstanceForCli(getProject()).getTrace();
|
||||
assertNotNull("No binding trace found for test", trace);
|
||||
|
||||
AnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
|
||||
getProject(), files, trace,
|
||||
Collections.<AnalyzerScriptParameter>emptyList(), Predicates.<PsiFile>alwaysTrue(), false).getBindingContext();
|
||||
|
||||
file.acceptChildren(new JetTreeVisitor<Data>() {
|
||||
|
||||
@Override
|
||||
public Void visitNamedFunction(@NotNull JetNamedFunction function, @Nullable Data outerData) {
|
||||
SimpleFunctionDescriptor descriptor = trace.get(BindingContext.FUNCTION, function);
|
||||
assert descriptor != null : "can't get function descriptor from binding by function declaration node";
|
||||
|
||||
Data data = new Data(descriptor);
|
||||
super.visitNamedFunction(function, data);
|
||||
|
||||
if (data.isTail) {
|
||||
List<JetCallExpression> calls = trace.get(BindingContext.FUNCTION_RECURSIVE_CALL_EXPRESSIONS, descriptor);
|
||||
if (calls == null) {
|
||||
calls = Collections.emptyList();
|
||||
}
|
||||
|
||||
List<JetCallExpression> detectedRecursions = new ArrayList<JetCallExpression>(calls);
|
||||
List<JetCallExpression> expectedRecursions = new ArrayList<JetCallExpression>(data.visitedCalls);
|
||||
|
||||
Collections.sort(detectedRecursions, new CallComparator());
|
||||
Collections.sort(expectedRecursions, new CallComparator());
|
||||
|
||||
assertEquals(
|
||||
"Bad detected tail recursions list for " + descriptor,
|
||||
stringListOfCallExpressions(expectedRecursions),
|
||||
stringListOfCallExpressions(detectedRecursions)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitCallExpression(@NotNull JetCallExpression expression, @Nullable Data data) {
|
||||
if (data != null && data.isTail) {
|
||||
ResolvedCall<? extends CallableDescriptor> call =
|
||||
trace.get(BindingContext.RESOLVED_CALL, expression.getCalleeExpression());
|
||||
|
||||
assert call != null : "call node is not yet resolved";
|
||||
if (data.functionDescriptor.equals(call.getCandidateDescriptor())) {
|
||||
JetValueArgumentList argumentList = expression.getValueArgumentList();
|
||||
assert argumentList != null : "function call have no arguments list";
|
||||
|
||||
checkCall(data.functionDescriptor, expression, argumentList, trace);
|
||||
data.visitedCalls.add(expression);
|
||||
}
|
||||
}
|
||||
|
||||
super.visitCallExpression(expression, data);
|
||||
return null;
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
private static String stringListOfCallExpressions(List<JetCallExpression> expectedRecursions) {
|
||||
return Joiner.on(",\n").skipNulls().join(Lists.transform(expectedRecursions, new CallExpressionToText()));
|
||||
}
|
||||
|
||||
private static class Data {
|
||||
public final SimpleFunctionDescriptor functionDescriptor;
|
||||
public final boolean isTail;
|
||||
public final List<JetCallExpression> visitedCalls = new ArrayList<JetCallExpression>();
|
||||
|
||||
private Data(@NotNull SimpleFunctionDescriptor descriptor) {
|
||||
functionDescriptor = descriptor;
|
||||
isTail = KotlinBuiltIns.getInstance().isTailRecursive(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkCall(
|
||||
SimpleFunctionDescriptor functionDescriptor,
|
||||
JetCallExpression expression,
|
||||
JetValueArgumentList argumentList,
|
||||
BindingTrace trace
|
||||
) {
|
||||
int size = argumentList.getArguments().size();
|
||||
boolean shouldBeTail = size == 0 || isLastArgumentTail(argumentList.getArguments());
|
||||
TailRecursionKind status = trace.get(BindingContext.TAIL_RECURSION_CALL, expression);
|
||||
assertNotNull("Call is not checked for tail recursion", status);
|
||||
assertEquals("Tail-recursion detection failed for " + functionDescriptor.getName().asString() + " at " + expression.getText(),
|
||||
shouldBeTail, status.isDoGenerateTailRecursion());
|
||||
}
|
||||
|
||||
private static boolean isLastArgumentTail(List<JetValueArgument> arguments) {
|
||||
JetValueArgument lastArgument = arguments.get(arguments.size() - 1);
|
||||
JetExpression expression = lastArgument.getArgumentExpression();
|
||||
if (expression instanceof JetStringTemplateExpression) {
|
||||
JetStringTemplateEntry[] entries = ((JetStringTemplateExpression) expression).getEntries();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (JetStringTemplateEntry entry : entries) {
|
||||
sb.append(entry.getText());
|
||||
}
|
||||
|
||||
return !sb.toString().trim().equals("no tail");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class CallComparator implements Comparator<JetCallExpression> {
|
||||
@Override
|
||||
public int compare(@NotNull JetCallExpression o1, @NotNull JetCallExpression o2) {
|
||||
return o1.getTextOffset() - o2.getTextOffset();
|
||||
}
|
||||
}
|
||||
|
||||
private static class CallExpressionToText implements Function<JetCallExpression, String> {
|
||||
@Override
|
||||
public String apply(JetCallExpression input) {
|
||||
if (input == null) return null;
|
||||
return ("\"" + input.getText().replace("\"", "\\\"").trim() + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2010-2013 JetBrains s.r.o.
|
||||
*
|
||||
* 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.jetbrains.jet.checkers;
|
||||
|
||||
import junit.framework.Assert;
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
import org.jetbrains.jet.JetTestUtils;
|
||||
import org.jetbrains.jet.test.InnerTestClasses;
|
||||
import org.jetbrains.jet.test.TestMetadata;
|
||||
|
||||
import org.jetbrains.jet.checkers.AbstractTailRecursionTest;
|
||||
|
||||
/** This class is generated by {@link org.jetbrains.jet.generators.tests.GenerateTests}. DO NOT MODIFY MANUALLY */
|
||||
@SuppressWarnings("all")
|
||||
@TestMetadata("compiler/testData/codegen/box/functions/tail-recursion")
|
||||
public class TailRecursionDetectorTestGenerated extends AbstractTailRecursionTest {
|
||||
public void testAllFilesPresentInTail_recursion() throws Exception {
|
||||
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.GenerateTests", new File("compiler/testData/codegen/box/functions/tail-recursion"), Pattern.compile("^(.+)\\.kt$"), true);
|
||||
}
|
||||
|
||||
@TestMetadata("defaultArgs.kt")
|
||||
public void testDefaultArgs() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/defaultArgs.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("insideElvis.kt")
|
||||
public void testInsideElvis() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/insideElvis.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("labeledThisReferences.kt")
|
||||
public void testLabeledThisReferences() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/labeledThisReferences.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("loops.kt")
|
||||
public void testLoops() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/loops.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("multilevelBlocks.kt")
|
||||
public void testMultilevelBlocks() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/multilevelBlocks.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("realIteratorFoldl.kt")
|
||||
public void testRealIteratorFoldl() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/realIteratorFoldl.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("realStringEscape.kt")
|
||||
public void testRealStringEscape() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/realStringEscape.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("realStringRepeat.kt")
|
||||
public void testRealStringRepeat() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/realStringRepeat.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("recursiveInnerFunction.kt")
|
||||
public void testRecursiveInnerFunction() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/recursiveInnerFunction.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("returnInCatch.kt")
|
||||
public void testReturnInCatch() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/returnInCatch.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("returnInFinally.kt")
|
||||
public void testReturnInFinally() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/returnInFinally.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("returnInIfInFinally.kt")
|
||||
public void testReturnInIfInFinally() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/returnInIfInFinally.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("returnInTry.kt")
|
||||
public void testReturnInTry() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/returnInTry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleBlock.kt")
|
||||
public void testSimpleBlock() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/simpleBlock.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleReturn.kt")
|
||||
public void testSimpleReturn() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/simpleReturn.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleReturnWithElse.kt")
|
||||
public void testSimpleReturnWithElse() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/simpleReturnWithElse.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("thisReferences.kt")
|
||||
public void testThisReferences() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/thisReferences.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unitBlocks.kt")
|
||||
public void testUnitBlocks() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/unitBlocks.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("whenWithCondition.kt")
|
||||
public void testWhenWithCondition() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/whenWithCondition.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("whenWithInRange.kt")
|
||||
public void testWhenWithInRange() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/whenWithInRange.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("whenWithIs.kt")
|
||||
public void testWhenWithIs() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/whenWithIs.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("whenWithoutCondition.kt")
|
||||
public void testWhenWithoutCondition() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/whenWithoutCondition.kt");
|
||||
}
|
||||
|
||||
}
|
||||
+120
-1
@@ -2361,7 +2361,7 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
|
||||
}
|
||||
|
||||
@TestMetadata("compiler/testData/codegen/box/functions")
|
||||
@InnerTestClasses({Functions.Invoke.class, Functions.LocalFunctions.class})
|
||||
@InnerTestClasses({Functions.Invoke.class, Functions.LocalFunctions.class, Functions.Tail_recursion.class})
|
||||
public static class Functions extends AbstractBlackBoxCodegenTest {
|
||||
public void testAllFilesPresentInFunctions() throws Exception {
|
||||
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.GenerateTests", new File("compiler/testData/codegen/box/functions"), Pattern.compile("^(.+)\\.kt$"), true);
|
||||
@@ -2623,11 +2623,130 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
|
||||
|
||||
}
|
||||
|
||||
@TestMetadata("compiler/testData/codegen/box/functions/tail-recursion")
|
||||
public static class Tail_recursion extends AbstractBlackBoxCodegenTest {
|
||||
public void testAllFilesPresentInTail_recursion() throws Exception {
|
||||
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.GenerateTests", new File("compiler/testData/codegen/box/functions/tail-recursion"), Pattern.compile("^(.+)\\.kt$"), true);
|
||||
}
|
||||
|
||||
@TestMetadata("defaultArgs.kt")
|
||||
public void testDefaultArgs() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/defaultArgs.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("insideElvis.kt")
|
||||
public void testInsideElvis() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/insideElvis.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("labeledThisReferences.kt")
|
||||
public void testLabeledThisReferences() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/labeledThisReferences.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("loops.kt")
|
||||
public void testLoops() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/loops.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("multilevelBlocks.kt")
|
||||
public void testMultilevelBlocks() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/multilevelBlocks.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("realIteratorFoldl.kt")
|
||||
public void testRealIteratorFoldl() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/realIteratorFoldl.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("realStringEscape.kt")
|
||||
public void testRealStringEscape() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/realStringEscape.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("realStringRepeat.kt")
|
||||
public void testRealStringRepeat() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/realStringRepeat.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("recursiveInnerFunction.kt")
|
||||
public void testRecursiveInnerFunction() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/recursiveInnerFunction.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("returnInCatch.kt")
|
||||
public void testReturnInCatch() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/returnInCatch.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("returnInFinally.kt")
|
||||
public void testReturnInFinally() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/returnInFinally.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("returnInIfInFinally.kt")
|
||||
public void testReturnInIfInFinally() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/returnInIfInFinally.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("returnInTry.kt")
|
||||
public void testReturnInTry() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/returnInTry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleBlock.kt")
|
||||
public void testSimpleBlock() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/simpleBlock.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleReturn.kt")
|
||||
public void testSimpleReturn() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/simpleReturn.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("simpleReturnWithElse.kt")
|
||||
public void testSimpleReturnWithElse() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/simpleReturnWithElse.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("thisReferences.kt")
|
||||
public void testThisReferences() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/thisReferences.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unitBlocks.kt")
|
||||
public void testUnitBlocks() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/unitBlocks.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("whenWithCondition.kt")
|
||||
public void testWhenWithCondition() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/whenWithCondition.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("whenWithInRange.kt")
|
||||
public void testWhenWithInRange() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/whenWithInRange.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("whenWithIs.kt")
|
||||
public void testWhenWithIs() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/whenWithIs.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("whenWithoutCondition.kt")
|
||||
public void testWhenWithoutCondition() throws Exception {
|
||||
doTest("compiler/testData/codegen/box/functions/tail-recursion/whenWithoutCondition.kt");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static Test innerSuite() {
|
||||
TestSuite suite = new TestSuite("Functions");
|
||||
suite.addTestSuite(Functions.class);
|
||||
suite.addTestSuite(Invoke.class);
|
||||
suite.addTestSuite(LocalFunctions.class);
|
||||
suite.addTestSuite(Tail_recursion.class);
|
||||
return suite;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user