The page of the Wiki is for developers who are interested in implementing the language in their own Java or Kotlin projects.
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
implementation("com.github.senseiwells:Arucas:2.0.2")
// Arucas relies on these dependancies, if you do not
// already implement these you need to add them.
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.10")
implementation("com.google.code.gson:gson:2.9.0")
}
After implementing Arucas you are ready to get started!
Arucas provides an API for you to be able to add your own built-in functions and classes, input/output handlers, library managers, obfuscation handling, and Java reflection conversions.
To get started we can create a basic Arucas API, we can use the API Builder to configure out API:
Kotlin:
import me.senseiwells.arucas.api.ArucasAPI
// ...
val api = ArucasAPI.Builder()
.addDefault()
.build()
//...
Java:
import me.senseiwells.arucas.api.ArucasAPI;
// ...
ArucasAPI api = new ArucasAPI.Builder()
.addDefault()
.build();
//...
The addDefault
method simply adds the default IO handler, default built-in functions and classes, and default Java reflection conversions.
You are also able to call these each individually:
Kotlin:
import me.senseiwells.arucas.api.ArucasAPI
// ...
val api = ArucasAPI.Builder()
.addDefaultLibrary()
.addDefaultConversions()
.addDefaultExtensions()
.addDefaultBuiltInDefinitions()
.addDefaultClassDefinitions()
.build()
//...
Java:
import me.senseiwells.arucas.api.ArucasAPI;
// ...
ArucasAPI api = new ArucasAPI.Builder()
.addDefaultLibrary()
.addDefaultConversions()
.addDefaultExtensions()
.addDefaultBuiltInDefinitions()
.addDefaultClassDefinitions()
.build();
//...
Now with an ArucasAPI
object we are able to run some Arucas code:
Kotlin:
import me.senseiwells.arucas.api.ArucasAPI
import me.senseiwells.arucas.api.ThreadHandler
import me.senseiwells.arucas.interpreter.Interpreter
// ...
val api = ArucasAPI.Builder()
.addDefault()
.build()
val code = "print('Hello World!');"
val interpreter = Interpreter.of(code, "My Code", api)
// We can run the interpreter async:
interpreter.executeAsync()
// Alternatively we can run it so it blocks the current thread:
// interpreter.executeBlocking()
//...
Java:
import me.senseiwells.arucas.api.ArucasAPI;
import me.senseiwells.arucas.api.ThreadHandler;
import me.senseiwells.arucas.interpreter.Interpreter;
// ...
ArucasAPI api=new ArucasAPI.Builder()
.addDefault()
.build();
String code="print('Hello World!');";
Interpreter interpreter=Interpreter.of(code,"My Code",api);
// We can run the interpreter async:
interpreter.executeAsync();
// Alternatively we can run it so it blocks the current thread:
// interpreter.executeBlocking();
//...
An extension provides you with the ability to create global functions for Arucas scripts.
Implementing one is quite simple, you must create a new class that implements ArucasExtension
:
Kotlin:
import me.senseiwells.arucas.api.ArucasExtension
class MyCustomExtension: ArucasExtension {
override fun getName() = "MyCustomExtension"
override fun getBuiltInFunctions(): List<BuiltInFunction> {
return listOf();
}
}
Java:
import me.senseiwells.arucas.api.ArucasExtension;
import java.util.List;
public class MyCustomExtension implements ArucasExtension {
@Override
public String getName() {
return "MyCustomExtension";
}
@Override
public List<BuiltInFunction> getBuiltInFunctions() {
return List.of();
}
}
You are able to define the functions you want in your extension and return them in the getBuiltInFunctions
method.
To create a BuiltInFunction
you can use the helper methods in the class. You are able to specify the name and parameters of your function, you are also able to define a function with a variable number of parameters. You are able to name multiple functions the same, given that they have a different number of parameters. Built-in functions also have the ability to support an arbitrary number of parameters.
Kotlin:
// ...
// 0 parameters
BuiltInFunction.of("something", { arguments ->
})
// 1 parameters
BuiltInFunction.of("something", 1, { arguments ->
})
// Arbitrary number of parameters
BuiltInFunction.arb("something", { arguments ->
})
// ...
Java:
// ...
// 0 parameters
BuiltInFunction.of("foo", arguments -> {
return null;
});
// 1 parameters
BuiltInFunction.of("foo", 1, arguments -> {
return null;
});
// Arbitrary number of parameters
BuiltInFunction.arb("something", arguments -> {
});
// ...
You can define the behaviour of the function with a lambda. This lambda passes in a parameter of type Arguments
, this object holds all the arguments that were passed into the function as well as the interpreter that the function was called from. The arguments will be of type ClassInstance
, these are Arucas objects.
The Arguments
object is flexible in how you can access the arguments, you have the ability to sequentially access the arguments as well as automatically cast them to the desired type. If you require a specific type and the argument is found to not be of the wanted type then an error will be thrown automatically.
Kotlin:
import me.senseiwells.arucas.builtin.StringDef
// ...
BuiltInFunction.of("bar", 3, { arguments ->
arguments.arguments // Gets all the arguments in a list
arguments.interpreter // Gets the interpreter
arguments.function // Gets the function that was called
arguments.size() // Gets the total number of arguments
arguments.get(0) // Gets an argument at an index 0
arguments.get(0, StringDef::class) // Gets argument at index 0, ensuring it is an instance of StringDef
arguments.getPrimitive(0, StringDef::class) // Gets argument at index 0 and casts it to a Kotlin String
arguments.next() // Gets the next argument (this starts at 0)
arguments.next(StringDef::class) // Gets the next argument, ensuring it is an instance of StringDef
arguments.nextPrimitive(StringDef::class) // Gets the next argument and casts it to a Kotlin String, this is probably the most used
arguments.skip() // Skips an argument
arguments.hasNext() // Checks whether there are any arguments left
arguments.isNext(StringDef::class) // Checks whether the next argument is an instance of StringDef
arguments.setIndex(0) // Sets the argument index
arguments.resetIndex() // Sets the argument index to 0
arguments.getRemaining() // Gets any remaining arguments as a list
})
// ...
Java:
import me.senseiwells.arucas.builtin.StringDef;
// ...
BuiltInFunction.of("bar", 3, arguments -> {
arguments.getArguments(); // Gets all the arguments in a list
arguments.getInterpreter(); // Gets the interpreter
arguments.getFunction(); // Gets the function that was called
arguments.size(); // Gets the total number of arguments
arguments.get(0); // Gets an argument at an index 0
arguments.get(0, StringDef.class); // Gets argument at index 0, ensuring it is an instance of StringDef
arguments.getPrimitive(0, StringDef.class); // Gets argument at index 0 and casts it to a Java String
arguments.next(); // Gets the next argument (this starts at 0)
arguments.next(StringDef.class); // Gets the next argument, ensuring it is an instance of StringDef
arguments.nextPrimitive(StringDef.class); // Gets the next argument and casts it to a Java String, this is probably the most used
arguments.skip(); // Skips an argument
arguments.hasNext(); // Checks whether there are any arguments left
arguments.isNext(StringDef.class); // Checks whether the next argument is an instance of StringDef
arguments.setIndex(0); // Sets the argument index
arguments.resetIndex(); // Sets the argument index to 0
arguments.getRemaining(); // Gets any remaining arguments as a list
return null;
});
// ...
You may also return values from this lambda. You can either return a value that is of ClassInstance
, or alternatively you can just return any Object
. The interpreter will try to convert any value that is not a ClassInstance
into one, for example it will turn a String
into a StringDef
instance. It does this through the ValueConverter
, if you are returning any object that does not have an Arucas definition already you need to add a conversion to your API, this is discussed in more detail in the the Java Support section.
Here is a full example:
Kotlin:
import me.senseiwells.arucas.api.ArucasExtension
import me.senseiwells.arucas.builtin.NumberDef
import me.senseiwells.arucas.builtin.StringDef
import me.senseiwells.arucas.functions.builtin.Arguments
class BuiltInExtension: ArucasExtension {
override fun getName() = "BuiltInExtension"
override fun getBuiltInFunctions(): List<BuiltInFunction> {
return listOf(
BuiltInFunction.of("print", 1, this::print),
BuiltInFunction.arb("sum", this::add)
)
}
private fun print(arguments: Arguments) {
println(arguments.nextPrimitive(StringDef::class))
}
private fun add(arguments: Arguments): Double {
var sum = 0.0
while (arguments.hasNext()) {
sum += arguments.nextPrimitive(NumberDef::class)
}
return sum
}
}
Java:
import me.senseiwells.arucas.api.ArucasExtension;
import me.senseiwells.arucas.builtin.NumberDef;
import me.senseiwells.arucas.builtin.StringDef;
import me.senseiwells.arucas.functions.builtin.Arguments;
import java.util.List;
public class BuiltInExtension implements ArucasExtension {
@Override
public String getName() {
return "BuiltInExtension";
}
@Override
public List<BuiltInFunction> getBuiltInFunctions() {
return List.of(
BuiltInFunction.of("print", 1, this::print),
BuiltInFunction.arb("sum", this::add)
);
}
private Void print(Arguments arguments) {
System.out.println(arguments.nextPrimitive(StringDef.class));
return null;
}
private double add(Arguments arguments) {
double sum = 0.0;
while (arguments.hasNext()) {
sum += arguments.nextPrimitive(NumberDef.class);
}
return sum;
}
}
Now that you've created an extension you'll want to add it to your ArucasAPI
:
Kotlin:
import me.senseiwells.arucas.api.ArucasAPI
// ...
val api = ArucasAPI.Builder()
.addDefault()
.addBuiltInExtension(BuiltInExtension())
.build()
//...
Java:
import me.senseiwells.arucas.api.ArucasAPI;
// ...
ArucasAPI api = new ArucasAPI.Builder()
.addDefault()
.addBuiltInExtension(new BuiltInExtension())
.build();
//...
Custom classes allow you to provide Objects
with defined behaviours for Arucas.
Creating a custom class is more complex than an extension but provides much more functionality. You can create one by either extendind the PrimitiveDefinition
or CreatableDefinition
class, you must provide the name of the class definition as well as the interpreter that it is being defined on. You can find an example below:
Kotlin:
import me.senseiwells.arucas.classes.PrimitiveDefinition
import me.senseiwells.arucas.interpreter.Interpreter
class MyCustomClassDef(interpreter: Interpreter): PrimitiveDefinition<Any>("MyCustomClass", interpreter) {
}
Java:
import me.senseiwells.arucas.classes.PrimitiveDefinition;
import me.senseiwells.arucas.interpreter.Interpreter;
public class MyCustomClassDef extends PrimitiveDefinition<Object> {
public MyCustomClassDef(Interpreter interpreter) {
super("MyCustomClass", interpreter);
}
}
Both PrimitiveDefinition
and CreatableDefinition
have a type parameter, this is because these class definition essentially act as a wrapper for a specific data type. For example if you were creating an Arucas class for Boolean
then you would pass Boolean
into the type parameter. For the example above Any
/Object
is used for a generic example.
Lets take a look at the difference between PrimitiveDefinition
and CreatableDefinition
. CreatableDefinition
extends PrimitiveDefinition
and changes one method: #create()
. CreatableDefinition
makes this method public allowing for the interpreter to be able to create instances of this class with an instance of the type parameter. There are cases were you would not want to be able to create new instances of your definition, for example Boolean
. There are only 2 possible boolean values in which case they can just be stored as constants instead of having the interpreter instantiate a new definition instance each time.
Adding constructors to your class definition is very similar to adding functions to an extension. You can override the defineConstructors
method and return a list of ConstructorFunction
. This function is very similar to the BuiltInFunction
that we discussed previously however the arguments will also include a ClassInstance
as the first argument (with any other arguments passed in following). You do not need to account this argument in your parameters
when creating the ConstructorFunction
.
Inside this constructor you must set the primitive value of the ClassInstance
, an example is shown below:
Kotlin:
import me.senseiwells.arucas.builtin.StringDef
import me.senseiwells.arucas.classes.CreatableDefinition
import me.senseiwells.arucas.interpreter.Interpreter
import me.senseiwells.arucas.functions.builtin.Arguments
import java.io.File
class FileDef(interpreter: Interpreter): CreatableDefinition<File>("File", interpreter) {
override fun defineConstructors(): List<ConstructorFunction> {
return listOf(
ConstructorFunction.of(1, this::construct)
)
}
private fun construct(arguments: Arguments) {
val instance = arguments.next()
val path = arguments.nextPrimitive(StringDef::class)
instance.setPrimitive(this, File(path))
}
}
Java:
import kotlin.Unit;
import me.senseiwells.arucas.builtin.StringDef;
import me.senseiwells.arucas.classes.ClassInstance;
import me.senseiwells.arucas.classes.CreatableDefinition;
import me.senseiwells.arucas.interpreter.Interpreter;
import me.senseiwells.arucas.functions.builtin.Arguments;
import java.io.File;
import java.util.List;
public class FileDef extends CreatableDefinition<File> {
public FileDef(Interpreter interpreter) {
super("File", interpreter);
}
@Override
public List<ConstructorFunction> defineConstructors() {
return List.of(
ConstructorFunction.of(1, this::construct)
);
}
private Unit construct(Arguments arguments) {
ClassInstance instance = arguments.next();
String path = arguments.nextPrimitive(StringDef.class);
instance.setPrimitive(this, new File(path));
return null;
}
}
Methods are also very straight forward to implement. To start you must override the defineMethods
method, which will return a list of MemberFunction
. Similarly to the ConstructorFunction
this function will have the ClassInstance
object refering to this
object as the first argument with any other arguments following. Like BuiltInFunctions
you may overload functions.
Here is an example:
Kotlin:
import me.senseiwells.arucas.builtin.StringDef
import me.senseiwells.arucas.classes.CreatableDefinition
import me.senseiwells.arucas.interpreter.Interpreter
import me.senseiwells.arucas.exceptions.runtimeError
import me.senseiwells.arucas.functions.builtin.Arguments
import java.io.File
import java.io.IOException
class FileDef(interpreter: Interpreter): CreatableDefinition<File>("File", interpreter) {
override fun defineMethods(): List<MemberFunction> {
return listOf(
MemberFunction.of("getName", this::getName),
MemberFunction.of("write", this::write)
)
}
private fun getName(arguments: Arguments): String {
val file = arguments.nextPrimitive(this)
return file.name
}
private fun write(arguments: Arguments) {
val file = arguments.nextPrimitive(this)
val string = arguments.nextPrimitive(StringDef::class)
try {
file.writeText(string)
} catch (e: IOException) {
runtimeError("There was an error writing the file '$file'", e)
}
}
}
Java:
import me.senseiwells.arucas.builtin.StringDef;
import me.senseiwells.arucas.classes.CreatableDefinition;
import me.senseiwells.arucas.interpreter.Interpreter;
import me.senseiwells.arucas.exceptions.RuntimeError;
import me.senseiwells.arucas.functions.builtin.Arguments;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
class FileDef extends CreatableDefinition<File> {
public FileDef(Interpreter interpreter) {
super("File", interpreter);
}
@Override
public List<MemberFunction> defineMethods() {
return List.of(
MemberFunction.of("getName", this::getName),
MemberFunction.of("write", this::write)
);
}
private String getName(Arguments arguments) {
File file = arguments.nextPrimitive(this);
return file.getName();
}
private Void write(Arguments arguments) {
File file = arguments.nextPrimitive(this);
String string = arguments.nextPrimitive(StringDef.class);
try {
Files.writeString(file.toPath(), string);
} catch (IOException e) {
throw new RuntimeError("There was an error writing the file '" + file + "'", e);
}
return null;
}
}
Now that you've created a class you'll want to add it to your ArucasAPI
, there are two ways you can add your custom class. Either as a 'Built-In' class, or an 'Importable' class. Having your class be built in means that scripts do not have to import the class, otherwise they need to import the class from the given import path.
Kotlin:
import me.senseiwells.arucas.api.ArucasAPI
// ...
val api = ArucasAPI.Builder()
.addDefault()
.addBuiltInDefinitions(::MyCustomClassDef)
// Or
.addClassDefinitions("import.path", ::MyCustomClassDef)
.build()
//...
Java:
import me.senseiwells.arucas.api.ArucasAPI;
// ...
ArucasAPI api = new ArucasAPI.Builder()
.addDefault()
.addBuiltInDefinitions(MyCustomClassDef::new)
// Or
.addClassDefinitions("import.path", MyCustomClassDef::new)
.build();
//...