/**
* Copyright (c) 2012-2016 André Bargull
* Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms.
*
* <https://github.com/anba/es6draft>
*/
package com.github.anba.es6draft.repl.global;
import static com.github.anba.es6draft.repl.SourceBuilder.ToSource;
import static com.github.anba.es6draft.repl.global.SharedFunctions.absolutePath;
import static com.github.anba.es6draft.repl.global.SharedFunctions.loadScript;
import static com.github.anba.es6draft.repl.global.SharedFunctions.readFile;
import static com.github.anba.es6draft.repl.global.SharedFunctions.relativePathToScript;
import static com.github.anba.es6draft.repl.global.WrapperProxy.CreateWrapProxy;
import static com.github.anba.es6draft.runtime.AbstractOperations.*;
import static com.github.anba.es6draft.runtime.types.Undefined.UNDEFINED;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import com.github.anba.es6draft.Executable;
import com.github.anba.es6draft.Script;
import com.github.anba.es6draft.compiler.CompilationException;
import com.github.anba.es6draft.parser.ParserException;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.Realm;
import com.github.anba.es6draft.runtime.internal.DebugInfo;
import com.github.anba.es6draft.runtime.internal.Errors;
import com.github.anba.es6draft.runtime.internal.Properties.Function;
import com.github.anba.es6draft.runtime.internal.Properties.Value;
import com.github.anba.es6draft.runtime.internal.ScriptException;
import com.github.anba.es6draft.runtime.internal.Source;
import com.github.anba.es6draft.runtime.modules.MalformedNameException;
import com.github.anba.es6draft.runtime.objects.FunctionPrototype;
import com.github.anba.es6draft.runtime.objects.GlobalObject;
import com.github.anba.es6draft.runtime.objects.collection.WeakMapObject;
import com.github.anba.es6draft.runtime.types.Callable;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.Type;
import com.github.anba.es6draft.runtime.types.builtins.BoundFunctionObject;
import com.github.anba.es6draft.runtime.types.builtins.BuiltinFunction;
import com.github.anba.es6draft.runtime.types.builtins.FunctionObject;
import com.github.anba.es6draft.runtime.types.builtins.ProxyObject;
/**
* Built-in functions for the mozilla-shell.
*/
public final class MozShellFunctions {
private final long startMilli = System.currentTimeMillis();
private final long startNano = System.nanoTime();
/**
* shell-function: {@code loadRelativeToScript(filename)}
*
* @param cx
* the execution context
* @param caller
* the caller execution context
* @param fileName
* the file path
*/
@Function(name = "loadRelativeToScript", arity = 1)
public void loadRelativeToScript(ExecutionContext cx, ExecutionContext caller, String fileName) {
Path file = Paths.get(fileName);
loadScript(cx, file, relativePathToScript(cx, caller, file));
}
/**
* shell-function: {@code evaluate(code, [options])}
*
* @param cx
* the execution context
* @param caller
* the caller context
* @param code
* the source code to evaluate
* @param options
* additional options object
* @return the eval result value
*/
@Function(name = "evaluate", arity = 2)
public Object evaluate(ExecutionContext cx, ExecutionContext caller, Object code, Object options) {
if (!(Type.isString(code) && (Type.isUndefined(options) || Type.isObject(options)))) {
throw Errors.newError(cx, "invalid arguments");
}
String sourceCode = Type.stringValue(code).toString();
String sourceName = "@evaluate";
int sourceLine = 1;
boolean noScriptRval = false;
boolean catchTermination = false;
Realm realm = cx.getRealm();
if (Type.isObject(options)) {
ScriptObject opts = Type.objectValue(options);
Object fileName = Get(cx, opts, "fileName");
if (!Type.isUndefined(fileName)) {
sourceName = Type.isNull(fileName) ? "" : ToFlatString(cx, fileName);
}
Object lineNumber = Get(cx, opts, "lineNumber");
if (!Type.isUndefined(lineNumber)) {
sourceLine = ToInt32(cx, lineNumber);
}
Object g = Get(cx, opts, "global");
if (!Type.isUndefined(g)) {
ScriptObject obj = ToObject(cx, g);
if (!(obj instanceof GlobalObject)) {
throw Errors.newError(cx, "invalid global argument");
}
realm = ((GlobalObject) obj).getRealm();
}
noScriptRval = ToBoolean(Get(cx, opts, "noScriptRval"));
catchTermination = ToBoolean(Get(cx, opts, "catchTermination"));
}
Source source = new Source(cx.getRealm().sourceInfo(caller), sourceName, sourceLine);
try {
Script script = realm.getScriptLoader().script(source, sourceCode);
Object result = script.evaluate(realm);
return (!noScriptRval ? result : UNDEFINED);
} catch (ParserException | CompilationException e) {
// Create a script exception from the requested code realm, not from the caller's realm.
throw e.toScriptException(realm.defaultContext());
} catch (ScriptException | StackOverflowError e) {
throw e;
} catch (Error | Exception e) {
if (catchTermination) {
return "terminated";
}
throw Errors.newError(cx, Objects.toString(e.getMessage(), ""));
}
}
/**
* shell-function: {@code run(file)}
*
* @param cx
* the execution context
* @param fileName
* the file to evaluate
* @return the execution time in milli-seconds
*/
@Function(name = "run", arity = 1)
public double run(ExecutionContext cx, String fileName) {
long start = System.nanoTime();
Path file = Paths.get(fileName);
loadScript(cx, file, absolutePath(cx, file));
long end = System.nanoTime();
return (double) TimeUnit.NANOSECONDS.toMillis(end - start);
}
/**
* shell-function: {@code printErr(message)}
*
* @param cx
* the execution context
* @param message
* the message to write
*/
@Function(name = "printErr", arity = 1)
public void printErr(ExecutionContext cx, String message) {
PrintWriter errorWriter = cx.getRuntimeContext().getConsole().errorWriter();
errorWriter.println(message);
}
/**
* shell-function: {@code putstr(message)}
*
* @param cx
* the execution context
* @param message
* the message to write
*/
@Function(name = "putstr", arity = 1)
public void putstr(ExecutionContext cx, String message) {
PrintWriter writer = cx.getRuntimeContext().getConsole().writer();
writer.print(message);
writer.flush();
}
/**
* shell-function: {@code dateNow()}
*
* @return the current date in micro-seconds resolution
*/
@Function(name = "dateNow", arity = 0)
public double dateNow() {
long elapsed = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startNano);
double date = startMilli + TimeUnit.MICROSECONDS.toMillis(elapsed);
double subdate = (elapsed % 1000) / 1000d;
return date + subdate;
}
/**
* shell-function: {@code assertEq()}
*
* @param cx
* the execution context
* @param actual
* the actual value
* @param expected
* the expected value
* @param message
* the optional error message
*/
@Function(name = "assertEq", arity = 2)
public void assertEq(ExecutionContext cx, Object actual, Object expected, Object message) {
if (!SameValue(actual, expected)) {
StringBuilder msg = new StringBuilder();
msg.append(String.format("Assertion failed: got %s, expected %s", ToSource(cx, actual),
ToSource(cx, expected)));
if (!Type.isUndefined(message)) {
msg.append(": ").append(ToFlatString(cx, message));
}
throw Errors.newError(cx, msg.toString());
}
}
/**
* shell-function: {@code throwError()}
*
* @param cx
* the execution context
*/
@Function(name = "throwError", arity = 0)
public void throwError(ExecutionContext cx) {
throw Errors.newError(cx, "This is an error");
}
/**
* shell-function: {@code evalcx(s, [o])}
*
* @param cx
* the execution context
* @param caller
* the caller context
* @param sourceCode
* the source to evaluate
* @param o
* the global object
* @return the eval result value
*/
@Function(name = "evalcx", arity = 1)
public Object evalcx(ExecutionContext cx, ExecutionContext caller, String sourceCode, Object o) {
ScriptObject global;
if (Type.isUndefinedOrNull(o)) {
global = newGlobal(cx);
} else {
global = ToObject(cx, o);
}
if (sourceCode.isEmpty() || "lazy".equals(sourceCode)) {
return global;
}
if (!(global instanceof GlobalObject)) {
throw Errors.newError(cx, "invalid global argument");
}
Source source = new Source(cx.getRealm().sourceInfo(caller), "evalcx", 1);
Realm realm = ((GlobalObject) global).getRealm();
try {
Script script = realm.getScriptLoader().script(source, sourceCode);
return script.evaluate(realm);
} catch (ParserException | CompilationException e) {
// Create a script exception from the requested code realm, not from the caller's realm.
throw e.toScriptException(realm.defaultContext());
}
}
/**
* shell-function: {@code sleep(dt)}
*
* @param cx
* the execution context
* @param dt
* the number of seconds to pause the application
*/
@Function(name = "sleep", arity = 1)
public void sleep(ExecutionContext cx, double dt) {
try {
TimeUnit.SECONDS.sleep(ToUint32(dt));
} catch (InterruptedException e) {
throw Errors.newError(cx, Objects.toString(e.getMessage(), ""));
}
}
/**
* shell-function: {@code snarf(filename)}
*
* @param cx
* the execution context
* @param fileName
* the file path
* @return the file content
*/
@Function(name = "snarf", arity = 1)
public String snarf(ExecutionContext cx, String fileName) {
Path file = Paths.get(fileName);
return readFile(cx, file, absolutePath(cx, file));
}
/**
* shell-function: {@code readRelativeToScript(filename)}
*
* @param cx
* the execution context
* @param caller
* the caller context
* @param fileName
* the file path
* @return the file content
*/
@Function(name = "readRelativeToScript", arity = 1)
public String readRelativeToScript(ExecutionContext cx, ExecutionContext caller, String fileName) {
Path file = Paths.get(fileName);
return readFile(cx, file, relativePathToScript(cx, caller, file));
}
/**
* shell-function: {@code elapsed()}
*
* @return the micro-seconds elapsed since application start-up
*/
@Function(name = "elapsed", arity = 0)
public double elapsed() {
return (double) TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startNano);
}
/**
* shell-function: {@code decompileFunction(function)}
*
* @param cx
* the execution context
* @param function
* the function object
* @return the function source string
*/
@Function(name = "decompileFunction", arity = 1)
public Object decompileFunction(ExecutionContext cx, Object function) {
if (!(function instanceof FunctionObject || function instanceof BuiltinFunction
|| function instanceof BoundFunctionObject)) {
return UNDEFINED;
}
return ((Callable) function).toSource(cx);
}
/**
* shell-function: {@code wrapWithProto(obj, proto)}
*
* @param cx
* the execution context
* @param obj
* the proxy target object
* @param proto
* the proxy prototype object
* @return the new proxy object
*/
@Function(name = "wrapWithProto", arity = 2)
public WrapperProxy wrapWithProto(ExecutionContext cx, Object obj, Object proto) {
return CreateWrapProxy(cx, obj, proto);
}
/**
* shell-function: {@code newGlobal()}
*
* @param cx
* the execution context
* @return a new global object instance
*/
@Function(name = "newGlobal", arity = 0)
public ScriptObject newGlobal(ExecutionContext cx) {
Realm realm;
try {
realm = cx.getRealm().getWorld().newInitializedRealm();
} catch (IOException | URISyntaxException e) {
throw Errors.newError(cx, Objects.toString(e.getMessage(), ""));
}
return realm.getGlobalThis();
}
/**
* shell-function: {@code getMaxArgs()}
*
* @return the maximum number of allowed function arguments
*/
@Function(name = "getMaxArgs", arity = 0)
public int getMaxArgs() {
return FunctionPrototype.getMaxArguments();
}
/**
* shell-function: {@code isProxy(p)}
*
* @param p
* the proxy object
* @return {@code true} if <var>p</var> is a proxy object
*/
@Function(name = "isProxy", arity = 1)
public boolean isProxy(Object p) {
return (p instanceof ProxyObject || p instanceof WrapperProxy);
}
/**
* shell-function: {@code parse(source)}
*
* @param cx
* the execution context
* @param source
* the source string to parse
*/
@Function(name = "parse", arity = 1)
public void parse(ExecutionContext cx, String source) {
cx.getRealm().getScriptLoader().parseScript(new Source("<script>", 1), source);
}
/**
* shell-function: {@code thisFilename()}
*
* @param cx
* the execution context
* @param caller
* the caller context
* @return the current file name
*/
@Function(name = "thisFilename", arity = 0)
public String thisFilename(ExecutionContext cx, ExecutionContext caller) {
Source source = cx.getRealm().sourceInfo(caller);
if (source == null) {
return "";
}
if (source.getFile() != null) {
return source.getFile().toString();
}
return source.getName();
}
/**
* shell-property: {@code scriptArgs}
*
* @param cx
* the execution context
* @return the array of arguments passed to the script
*/
@Value(name = "scriptArgs")
public Object scriptArgs(ExecutionContext cx) {
return Get(cx, cx.getGlobalObject(), "arguments");
}
/**
* shell-function: {@code objectAddress(object)}
*
* @param cx
* the execution context
* @param object
* the script object
* @return the object's address
*/
@Function(name = "objectAddress", arity = 1)
public String objectAddress(ExecutionContext cx, ScriptObject object) {
return Integer.toString(System.identityHashCode(object), 16);
}
/**
* shell-function: {@code nondeterministicGetWeakMapKeys(weakMap)}
*
* @param cx
* the execution context
* @param weakMap
* the weak map
* @return the object's address
*/
@Function(name = "nondeterministicGetWeakMapKeys", arity = 1)
public ScriptObject nondeterministicGetWeakMapKeys(ExecutionContext cx, WeakMapObject weakMap) {
return CreateArrayFromList(cx, weakMap.getWeakMapData().keySet());
}
/**
* shell-function: {@code dis([function])}
*
* @param cx
* the execution context
* @param caller
* the caller context
* @param args
* the arguments
* @throws IOException
* if there was any I/O error
* @throws MalformedNameException
* if the module name cannot be normalized
*/
@Function(name = "dis", arity = 1)
public void dis(ExecutionContext cx, ExecutionContext caller, Object... args)
throws IOException, MalformedNameException {
DebugInfo debugInfo = debugInfo(caller, args);
if (debugInfo != null) {
PrintWriter writer = cx.getRuntimeContext().getConsole().writer();
for (DebugInfo.Method method : debugInfo.getMethods()) {
writer.println(method.disassemble());
}
}
}
/**
* shell-function: {@code disassemble([function])}
*
* @param cx
* the execution context
* @param caller
* the caller context
* @param args
* the arguments
* @return the disassembled byte code
* @throws IOException
* if there was any I/O error
* @throws MalformedNameException
* if the module name cannot be normalized
*/
@Function(name = "disassemble", arity = 1)
public String disassemble(ExecutionContext cx, ExecutionContext caller, Object... args)
throws IOException, MalformedNameException {
DebugInfo debugInfo = debugInfo(caller, args);
if (debugInfo != null) {
StringBuilder sb = new StringBuilder();
for (DebugInfo.Method method : debugInfo.getMethods()) {
sb.append(method.disassemble()).append('\n');
}
return sb.toString();
}
return "";
}
private static DebugInfo debugInfo(ExecutionContext caller, Object... args) {
if (args.length == 0) {
FunctionObject currentFunction = caller.getCurrentFunction();
Executable currentExec = caller.getCurrentExecutable();
if (currentFunction != null && currentFunction.getExecutable() == currentExec) {
return currentFunction.getCode().debugInfo();
} else if (currentExec != null && currentExec.getSourceObject() != null) {
return currentExec.getSourceObject().debugInfo();
}
} else if (args[0] instanceof FunctionObject) {
return ((FunctionObject) args[0]).getCode().debugInfo();
}
return null;
}
}