/* --------------------------------------------------------- *
* __________ D E L T A S C R I P T *
* (_________() *
* / === / - A fast, dynamic scripting language *
* | == | - Version 4.13.11.0 *
* / === / - Developed by Adam R. Nelson *
* | = = | - 2011-2013 *
* / === / - Distributed under GNU LGPL v3 *
* (________() - http://github.com/ar-nelson/deltascript *
* *
* --------------------------------------------------------- */
package com.sector91.delta.script;
import static com.sector91.delta.script.objects.DS_Tag.tag;
import com.sector91.delta.script.instrs.DSInstr;
import com.sector91.delta.script.objects.DS_Array;
import com.sector91.delta.script.objects.DS_Blank;
import com.sector91.delta.script.objects.DS_Boolean;
import com.sector91.delta.script.objects.DS_Callable;
import com.sector91.delta.script.objects.DS_List;
import com.sector91.delta.script.objects.DS_Map;
import com.sector91.delta.script.objects.DS_Object;
import com.sector91.delta.script.objects.DS_Scalar;
import com.sector91.delta.script.objects.DS_Scope;
import com.sector91.delta.script.objects.DS_Set;
import com.sector91.delta.script.objects.DS_String;
import com.sector91.delta.script.objects.DS_Tag;
import com.sector91.delta.script.objects.ScalarFactory;
import com.sector91.delta.script.objects.VectorFactory;
import com.sector91.delta.script.objects.reflect.DS_JavaClass;
import com.sector91.delta.script.objects.reflect.DS_JavaObject;
import com.sector91.delta.script.parser.DScriptBinaryReader;
import com.sector91.delta.script.parser.DScriptParserException;
import com.sector91.delta.script.parser.DScriptParser;
import com.sector91.geom.Shape;
import com.sector91.geom.Vector;
import com.sector91.util.Sector91Version;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* <p>Contains static utility methods used to compile and execute DeltaScript
* code.</p>
*
* @author Adam R. Nelson
* @version 4.13.11.0
*/
public final class DeltaScript
{
// Static Methods
// ----------------------------------------------------
/** The name of the scripting language. */
public static final String NAME = "DeltaScript";
/** Guess who! */
public static final String AUTHOR = "Adam R. Nelson";
/** The DeltaScript version that this class represents. */
public static final Sector91Version VERSION =
new Sector91Version(4, 13, 11, 0);
/** The website of DeltaScript's creator. */
public static final String WEBSITE = "http://github.com/ar-nelson/deltascript";
/** The file encoding of DeltaScript files. */
public static final Charset FILE_ENCODING = Charset.forName("UTF-8");
/** The extension of DeltaScript source files: {@code .ds} */
public static final String SRC_EXT = ".ds";
/** The extension of DeltaScript binary files: {@code .dsc} */
public static final String BIN_EXT = ".dsc";
/** The extension of DeltaScript module files: {@code .dsm} */
public static final String MODULE_EXT = ".dsm";
/** The name of the "sentinel" file that marks a folder as a DeltaScript
* module: {@code module.ds} */
public static final String MODULE_FILENAME = "module.ds";
/** The name of the function that, if it exists, will be executed after
* all of the subfiles of a module have been loaded: {@code main} */
public static final DS_Tag MODULE_MAIN_FUNC_NAME = tag("main");
/** The name of the callable that will be called in the event of an error.*/
public static final DS_Tag ERROR_FUNC_NAME = tag("error");
/** The DeltaScript Standard Library. */
public static final DS_JavaClass STDLIB = DS_JavaClass.fromClass(
DScriptStandardLibrary.class);
public static final int SCOPE_HASH_BUCKETS = 16;
private DeltaScript() {}
/**
* <p>Compiles DeltaScript source code into a {@link CompiledDeltaScript}
* object, which can be executed faster (using
* {@link #exec(CompiledDeltaScript, DS_Scope)}) than interpreting raw
* source code through {@link #eval(String, String, DS_Scope)}.</p>
*
* @param scriptText The DeltaScript source to compile.
* @return A CompiledDeltaScript, which can be run with {@code exec}.
* @throws DScriptErr If something goes wrong parsing the code.
*/
public static DSInstr compile(String scriptText)
throws DScriptParserException
{return compile(scriptText, new DScriptContext());}
/**
* <p>Compiles DeltaScript source code into a {@link CompiledDeltaScript}
* object, which can be executed faster (using
* {@link #exec(CompiledDeltaScript, DS_Scope)}) than interpreting raw
* source code through {@link #eval(String, String, DS_Scope)}.</p>
*
* @param scriptText The DeltaScript source to compile.
* @param context The context in which to compile the code.
* @return A CompiledDeltaScript, which can be run with {@code exec}.
* @throws DScriptErr If something goes wrong parsing the code.
*/
public static DSInstr compile(String scriptText, DScriptContext context)
throws DScriptParserException
{return compile(scriptText, "Unnamed Script", context);}
/**
* <p>Compiles DeltaScript source code into a {@link CompiledDeltaScript}
* object, which can be executed faster (using {@link #exec(
* CompiledDeltaScript, DS_Scope)}) than interpreting raw source code
* through {@link #eval(String, String, DS_Scope)}.</p>
*
* @param scriptText The DeltaScript source to compile.
* @param scriptName The name of the script, for debugging purposes.
* @return A CompiledDeltaScript, which can be run with {@code exec}.
* @throws DScriptErr If something goes wrong parsing the code.
*/
public static DSInstr compile(String scriptText, String scriptName)
throws DScriptParserException
{return compile(scriptText, scriptName, new DScriptContext());}
/**
* <p>Compiles DeltaScript source code into a {@link CompiledDeltaScript}
* object, which can be executed faster (using {@link #exec(
* CompiledDeltaScript, DS_Scope)}) than interpreting raw source code
* through {@link #eval(String, String, DS_Scope)}.</p>
*
* @param scriptText The DeltaScript source to compile.
* @param scriptName The name of the script, for debugging purposes.
* @param context The context in which to compile the code.
* @return A CompiledDeltaScript, which can be run with the DScriptEngine.
* @throws DScriptErr If something goes wrong parsing the code.
*/
public static DSInstr compile(String scriptText, String scriptName,
DScriptContext context) throws DScriptParserException
{return new DScriptParser(context).compile(scriptText, scriptName);}
/**
* <p>Evaluates DeltaScript source code. The code is compiled into a {@link
* CompiledDeltaScript} object, which is then executed in a new scope.</p>
*
* @param scriptText The DeltaScript source to execute.
* @param scriptName The name of the script, for debugging purposes.
* @return The return value of the last evaluated statement in the script.
* @throws DScriptErr If any error occurs in parsing or executing the
* script.
*/
public static DS_Object eval(String scriptText, String scriptName)
throws DScriptErr, DScriptParserException
{
final DScriptContext context = new DScriptContext();
return eval(scriptText, scriptName, context.createScope(), context);
}
/**
* <p>Evaluates DeltaScript source code. The code is compiled into a {@link
* CompiledDeltaScript} object, which is then executed in the given scope.
* </p>
*
* <p>Equivalent to the following code: {@code
* DScriptEngine.exec(DScriptEngine.compile(scriptText, scriptName), scope);
* }</p>
*
* @param scriptText The DeltaScript source to execute.
* @param scriptName The name of the script, for debugging purposes.
* @param scope The scope to execute the script in.
* @return The return value of the last evaluated statement in the script.
* @throws DScriptErr If any error occurs in parsing or executing the
* script.
*/
public static DS_Object eval(String scriptText, String scriptName,
DS_Scope scope) throws DScriptErr, DScriptParserException
{return eval(scriptText, scriptName, scope, new DScriptContext());}
/**
* <p>Evaluates DeltaScript source code. The code is compiled into a {@link
* CompiledDeltaScript} object, which is then executed in the given scope.
* </p>
*
* <p>Equivalent to the following code:
* <pre>
* DScriptEngine.exec(
* DScriptEngine.compile(scriptText, scriptName, context),
* scope, context);
* </pre></p>
*
* @param scriptText The DeltaScript source to execute.
* @param scriptName The name of the script, for debugging purposes.
* @param scope The scope to execute the script in.
* @param context The context in which to compile and execute the script.
* @return The return value of the last evaluated statement in the script.
* @throws DScriptErr If any error occurs in parsing or executing the
* script.
*/
public static DS_Object eval(String scriptText, String scriptName,
DS_Scope scope, DScriptContext context) throws DScriptErr,
DScriptParserException
{return exec(compile(scriptText, scriptName, context), scope, context);}
/**
* <p>Executes compiled DeltaScript scripts in the given scope.</p>
*
* @param script The compiled DeltaScript script to execute.
* @param scope The scope to execute the script in.
* @return The return value of the last evaluated statement in the script.
* @throws DScriptErr If any error occurs in executing the script.
*/
public static DS_Object exec(DSInstr instr, DS_Scope scope)
throws DScriptErr
{return exec(instr, scope, null);}
/**
* <p>Executes compiled DeltaScript scripts in a new scope in the given
* context.</p>
*
* @param script The compiled DeltaScript script to execute.
* @param context The {@link DScriptContext} to execute the script in.
* @return The return value of the last evaluated statement in the script.
* @throws DScriptErr If any error occurs in executing the script.
*/
public static DS_Object exec(DSInstr instr, DScriptContext context)
throws DScriptErr
{return exec(instr, context.createScope(), context);}
/**
* <p>Executes compiled DeltaScript scripts in the given scope and context.
* </p>
*
* @param script The compiled DeltaScript script to execute.
* @param scope The scope to execute the script in.
* @param context The {@link DScriptContext} to execute the script in.
* @return The return value of the last evaluated statement in the script.
* @throws DScriptErr If any error occurs in executing the script.
*/
public static DS_Object exec(DSInstr instr, DS_Scope scope,
DScriptContext context) throws DScriptErr
{
DScriptExecutor exc = new DScriptExecutor(instr,scope,false,context);
return exc.execInstance();
}
/**
* <p>Mostly for internal use. Unless you know what you're doing, it's
* recommended that you use {@link #exec(CompiledDeltaScript, DS_Scope)}
* instead.</p>
*
* @param script The compiled DeltaScript script to execute.
* @param scope The scope to execute the script in.
* @param context The {@link DScriptContext} to execute the script in.
* @return The return value of the last evaluated statement in the script.
* @throws DScriptErr If any error occurs in executing the script.
*/
public static DS_Object execAsFunctionCall(DSInstr instr, DS_Scope scope,
DScriptContext context) throws DScriptErr
{
DScriptExecutor exc = new DScriptExecutor(instr, scope, true, context);
return exc.execInstance();
}
/**
* <p>Loads and executes a DeltaScript source/binary file or package in the
* given context.</p>
*
* <p>If {@code context} is non-null and {@code f} is a relative path,
* {@code f} will be resolved <em>relative to the current working directory
* of the context</em>, not to the current working directory of the JVM. The
* file will be executed in a new scope with a new context based off of
* {@code context}, but with its current working directory set to the parent
* directory of the script file.</p>
*
* @param f The {@link File} to load the script from. It must be either a
* file with the extension {@link #SRC_EXT} or {@link #BIN_EXT},
* or a folder containing a file named {@link #PACKAGE_FILENAME}.
* @param context The context to execute the script in.
* @param returnScope If true, this method will return the scope that the
* file was executed in, rather than the return value of the last
* statement executed
* @return Depending on {@code returnScope}, either the scope that the file
* was executed in, or the return value of the last evaluated
* statement in the script.
* @throws IOException If an error occurs while loading/accessing the script
* file, or if the file is not a DeltaScript file.
* @throws DScriptErr If a runtime error occurs while executing the
* script, or if the given context does not have permission to load
* script files.
* @throws DScriptParserException If an error occurs while parsing the
* script file.
* @since 3.12.3.0
* @see #execModule(File, DS_Scope)
*/
public static DS_Object execFile(File f, DScriptContext context,
boolean returnScope) throws IOException, DScriptParserException
{
if (!f.isAbsolute() && context != null)
{
// Use the Current Working Directory of the DScriptContext,
// *not* of the Java VM!
f = new File(
context.getCurrentWorkingDirectory(), f.getPath()
).getAbsoluteFile();
}
//if (f.isDirectory())
// return execModule(f, scope);
final DScriptContext newContext;
if (context != null)
newContext = context.clone();
else
newContext = new DScriptContext();
newContext.setCurrentWorkingDirectory(f.getParentFile());
DSInstr script;
final FileInputStream fileStream = new FileInputStream(f);
try
{
if (f.getName().toLowerCase().endsWith(SRC_EXT))
{
final BufferedReader br = new BufferedReader(
new InputStreamReader(fileStream, FILE_ENCODING));
final StringBuffer data = new StringBuffer();
for (String ln=br.readLine(); ln != null; ln=br.readLine())
data.append(ln+"\n");
script = compile(data.toString(), f.getName(), newContext);
}
else if (f.getName().toLowerCase().endsWith(BIN_EXT))
script = new DScriptBinaryReader(fileStream).read(
newContext, f.getName());
else throw new IOException("The file \"" + f.getName() + "\" is" +
" not a DeltaScript file. DeltaScript filenames must end with" +
" \"" + SRC_EXT + "\" or \"" + BIN_EXT + "\".");
}
finally
{fileStream.close();}
final DS_Scope scope = newContext.createScope();
final DS_Object retval = exec(script, scope, newContext);
return returnScope ? scope : retval;
}
/**
* <p>Loads and executes a DeltaScript module folder in the given scope.</p>
*
* <p>A folder is considered a DeltaScript module if it contains a file
* named {@link #MODULE_FILENAME}. This file will be executed first, in a
* the given scope. After this, any files in the folder with the
* {@link #SRC_EXT} or {@link #BIN_EXT} extensions will be loaded, in
* lexicographic order, into subscopes of this scope with the same names
* as the files, using {@link #execFile(File, DS_Scope)}. Finally, if the
* package scope contains a function named {@link #PACKAGE_MAIN_FUNC_NAME},
* that function will be called.</p>
*
* <p>If {@code scope} has an associated {@link DScriptContext} and {@code
* f} is a relative path, {@code f} will be resolved <em>relative to the
* current working directory of the {@code DScriptContext}</em>, not to the
* current working directory of the JVM. The given scope will be associated
* with a new {@code DScriptContext} instance with its current working
* directory set to the package folder.</p>
*
* @param f The {@link File} to load the script from. It must represent a
* folder, and the folder must contain a file named
* {@link #PACKAGE_FILENAME}.
* @param scope The scope to execute the script in.
* @return The given scope, after the execution has finished.
*/
public static DS_Scope execModule(File f, DS_Scope scope)
throws DScriptParserException
{
throw new UnsupportedOperationException();
/*if (!f.isAbsolute() && scope.getEngineInstance() != null)
{
// Use the Current Working Directory of the DScriptEngine,
// *not* of the Java VM!
f = new File(
scope.getEngineInstance().getContext()
.getCurrentWorkingDirectory(),
f.getPath()
).getAbsoluteFile();
} else f = f.getAbsoluteFile();
if (!f.isDirectory())
throw new DScriptException("The path \"" + f.getPath() + "\" is not" +
" a DeltaScript package, because it does not represent" +
" a directory. DeltaScript packages are directories" +
" containing a file named " + PACKAGE_FILENAME + ".");
// Execute the package root file.
final File[] files = f.listFiles();
boolean foundPackageFile = false;
for (File subfile : files)
{
if (subfile.isFile() &&
PACKAGE_FILENAME.equals(subfile.getName()))
{
execFile(subfile, scope); // This will associate the current
// scope with a new DScriptEngine.
foundPackageFile = true;
break;
}
}
if (!foundPackageFile)
throw new DScriptException("The directory \"" + f.getPath() + "\" is" +
" not a DeltaScript package, because it does not contain" +
" a file named " + PACKAGE_FILENAME + ".");
// Execute package files in subscopes.
List<DS_Scope> scopes = new ArrayList<DS_Scope>();
List<String> scopeNames = new ArrayList<String>();
for (File subfile : files)
{
final String filename = subfile.getName();
if (filename.endsWith(SRC_EXT) || filename.endsWith(BIN_EXT))
{
if (PACKAGE_FILENAME.equals(filename)) continue;
// If there are both binary and source versions of a file,
// go with the source version.
final String nameWithoutExt =
filename.substring(0, filename.lastIndexOf('.'));
if (filename.endsWith(BIN_EXT))
{
File dupFile = null;
for (File potentialDup : files)
{
if (potentialDup.getName().equals(
nameWithoutExt+SRC_EXT))
{dupFile = potentialDup; break;}
}
if (dupFile != null) continue;
}
// Execute the file in a named subscope, but don't add it to
// the package scope yet! Files are loaded in an arbtrary
// order, and should never depend on each other's existence
// while loading.
final DS_Scope fileScope = scope.createSubscope();
execFile(subfile, fileScope);
scopes.add(fileScope);
scopeNames.add(identifierize(nameWithoutExt));
}
}
// _Now_ add the scopes to the package scope.
for (int i=0; i<scopes.size(); i++)
scope.set(tag(scopeNames.get(i)), scopes.get(i));
System.out.println(scope);
// If the package has a main function, call it now, after everything
// has loaded.
DS_Object mainFunc = scope.getLocal(PACKAGE_MAIN_FUNC_NAME);
if (mainFunc instanceof DS_Callable)
((DS_Callable)mainFunc).call(scope);
// Return the now-populated scope.
return scope;*/
}
/**
* <p>Calls a named function, closure or other callable in the given scope
* and returns its result.</p>
*
* @param name The identifier of the defined callable to call.
* @param scope The scope in which to find the callable and execute it.
* @param args The arguments (if any) to call the callable with.
* @return The return value of the callable.
* @throws DScriptErr If the given identifier is not defined or not
* callable, or if another error occurs.
*/
public static DS_Object call(String name, DS_Scope scope,
DS_Object... args) throws DScriptErr
{
DS_Object var = scope.dotGet(DS_Tag.tag(name));
if (var == null)
throw new DScriptErr(name + " is undefined in this scope.");
if (!(var instanceof DS_Callable))
throw new DScriptErr(name + " is not callable.");
return ((DS_Callable)var).call(args);
}
/**
* <p>Takes a Java object and <i>boxes</i> it as a {@link DS_Object}. It
* tries built-in mappings of Java objects to DeltaScript objects first,
* and, if that doesn't work, checks for user-defined mappings, which can
* be defined with {@link #addBoxMapping(Class, Constructor)}. If no mapping
* is found, this method defaults to boxing the object as a
* {@link DS_JavaObject}.</p>
*
* <p>The inverse of this operation is calling {@link
* DS_Object#unbox()}.</p>
*
* @param obj The object to box.
* @return A {@link DS_Object} that either contains the given object or is
* roughly equivalent to it.
*/
public static DS_Object box(Object obj)
{
if (obj instanceof DS_Object)
return (DS_Object)obj;
else if (obj == null)
return DS_Blank.BLANK;
else if (obj instanceof Boolean)
return DS_Boolean.box((Boolean)obj);
else if (obj instanceof List<?>)
{
List<?> src = (List<?>)obj;
DS_List dst = new DS_List();
for (int i=0; i<src.size(); i++)
dst.add(box(src.get(i)));
return dst;
}
else if (obj instanceof Map<?,?>)
{
Map<?,?> src = (Map<?,?>)obj;
DS_Map dst = new DS_Map();
for (Map.Entry<?,?> e : src.entrySet())
dst.put(box(e.getKey()), box(e.getValue()));
return dst;
}
else if (obj instanceof Set<?>)
{
Set<?> src = (Set<?>)obj;
DS_Set dst = new DS_Set();
for (Object item : src)
dst.add(box(item));
return dst;
}
else if (obj instanceof Vector)
return DScriptContext.getContextGeometry().convert((Vector)obj);
else if (obj instanceof Shape)
return DScriptContext.getContextGeometry().convert((Shape)obj);
final Class<?> cls = obj.getClass();
if (cls == String.class)
return new DS_String((String)obj);
else if (obj instanceof Number)
{
final Number n = (Number)obj;
if (cls == Long.class)
return ScalarFactory.fromLong(n.longValue());
else if (cls == Float.class)
return ScalarFactory.fromFloat(n.floatValue());
else if (cls == Integer.class ||
cls == Short.class ||
cls == Byte.class)
return ScalarFactory.fromInt(n.intValue());
else if (cls == Double.class)
return ScalarFactory.fromDouble(n.doubleValue());
else if (cls == BigInteger.class)
return ScalarFactory.fromBigInteger((BigInteger)n);
else if (cls == BigDecimal.class)
return ScalarFactory.fromBigDecimal((BigDecimal)n);
throw new UnsupportedOperationException();
}
else if (cls.isArray())
{
if (cls == float[].class)
return VectorFactory.fromArray((float[])obj,
NumberTypes.SHORT_FLOAT);
else if (cls == int[].class)
return VectorFactory.fromArray((int[])obj,
NumberTypes.SHORT_INT);
else if (cls == double[].class)
return VectorFactory.fromArray((double[])obj,
NumberTypes.LONG_FLOAT);
else if (cls == long[].class)
return VectorFactory.fromArray((long[])obj,
NumberTypes.LONG_INT);
else if (cls == boolean[].class)
return VectorFactory.bitVector((boolean[])obj);
// TODO: BigInteger, BigDecimal
DS_Object[] arr = new DS_Object[Array.getLength(obj)];
for (int i=0; i<arr.length; i++)
arr[i] = DeltaScript.box(Array.get(obj, i));
return new DS_Array(arr);
}
else if (DScriptContext.get() != null)
{
Map<Class<?>, Converter<?, ?>> conversions =
DScriptContext.get().getConversions();
if (conversions.containsKey(cls))
{
try
{
return ((Converter<Object, ?>)conversions.get(
obj.getClass())).convert(obj);
}
catch (Exception ex)
{throw new RuntimeException("Failed to box type " +
obj.getClass().getName() + ": " + ex.getMessage(), ex);}
}
}
return new DS_JavaObject<Object>(obj);
}
public static float floatValue(DS_Object var) throws DScriptErr
{
if (var instanceof DS_Scalar)
return ((DS_Scalar)var).floatValue();
else
throw new DScriptErr("Scalar expected.",
DScriptErr.T_INVALID_TYPE, DScriptErr.T_NOT_NUMBER);
}
public static int intValue(DS_Object var) throws DScriptErr
{
if (var instanceof DS_Scalar &&
((DS_Scalar)var).isIntegral())
return ((DS_Scalar)var).intValue();
else
throw new DScriptErr("Integer expected.",
DScriptErr.T_INVALID_TYPE,DScriptErr.T_NOT_INTEGER);
}
public static long longValue(DS_Object var) throws DScriptErr
{
if (var instanceof DS_Scalar &&
((DS_Scalar)var).isIntegral())
return ((DS_Scalar)var).longValue();
else
throw new DScriptErr("Integer expected.",
DScriptErr.T_INVALID_TYPE,DScriptErr.T_NOT_INTEGER);
}
/**
* <p>Converts {@code str} into a valid DeltaScript identifier. Any
* non-alphanumeric characters in the string are replaced by underscores,
* and an underscore is added to the beginning of strings that start with
* numbers.</p>
*
* @param str The string to convert into a DeltaScript identifier.
* @return A valid DeltaScript identifier, based on {@code str}.
*/
public static String identifierize(String str)
{
final String identifier = str.replaceAll("[^\\w]", "_");
if (identifier.matches("^\\d.*$"))
return "_" + identifier;
else
return identifier;
}
}