Marcel, a simple and efficient programming language for any JVM

Marcel is a programming language built with the following goals in mind:

  • Being simple, not too verbose, allowing to write programs quickly
  • Runnable on Android devices (this language is guaranteed to be compilable and executable on Android devices)

Its features are inspired from many languages such as Groovy, Kotlin, Perl and Dart. You can consult the source code of this language on GitHub


Marcel compiles to Java bytecode (.class files). You can execute marcel script/projects on any JVMs (17+), as long as the Marcel stdlib is included in your classpath.

As stated above, marcel is guaranteed to be compilable and executable on any Android devices running on Android 11 or +. You can install the Marcel for Android app to develop, compile and execute Marcel programs.

Getting Started

In this section we'll discuss:

  • Installing Marcel
  • Writing and running a Marcel program that prints Hello World

Installation

Marcel comes with

For now, the only way to install Marcel is compiling it from its source code.

Install from release

You can run the below command in order to install a Marcel release in a specific directory.

curl -s https://raw.githubusercontent.com/tambapps/marcel/main/install/install-from-release.sh | bash -s

You can download the release.zip of a Marcel Release. Unzip it where you want.

Then set the MARCEL_HOME environment to the path of the release you just unzipped. You can set it in your .bashrc or .zshrc so that this variable is set in all your sessions. You can also add $MARCEL_HOME/bin to your PATH so your shell recognize marcel commands.

Install from source code

There is a script in marcel repository for that.

Note that this script only works on Linux and Mac and requires Maven being installed.

git clone https://github.com/tambapps/marcel.git
cd marcel
./install/install-from-source.sh

Prerequisites

You'll need Maven for the installation.

Run script

Clone the repository

git clone https://github.com/tambapps/marcel.git
cd marcel

And then run the script

./install-from-source.sh

The script basically runs a lot of mvn clean package and then copy/create some files in $HOME/.marcel/.

Lastly, you can add the following lines into your $HOME/.bashrc (or $HOME/.zshrc or whatever) to easily use marcel tools

MARCEL_HOME="$HOME/.marcel"
PATH="$PATH:$MARCEL_HOME/bin"

Hello World

Let's dive in some code now. Marcel can be used as a scripting language, so you don't need to declare a main() function if you just want to run some code.

// HelloWorld.mcl
println("Hello World!")

That's it! One line is all it takes to write the famous Hello World program.

To execute it, use marcl.

marcl HelloWorld.mcl

Language Specification

In this section, you'll learn all the little secrets of MarcelLang.

Syntax

This chapter covers the syntax of the Marcel programming language.

Keywords

Marcel has the following keywords, which you cannot use as variable/function/fields names

  • async
  • int
  • dynobj
  • do
  • long
  • short
  • float
  • double
  • bool
  • byte
  • void
  • char
  • fun
  • return
  • true
  • false
  • new
  • import
  • as
  • inline
  • static
  • for
  • in
  • if
  • else
  • null
  • break
  • continue
  • def
  • class
  • extension
  • package
  • extends
  • implements
  • final
  • switch
  • when
  • this
  • super
  • dumbbell
  • try
  • catch
  • finally
  • instanceof
  • throw
  • throws
  • constructor
  • public
  • protected
  • internal
  • private
  • while

Comments

You can comment your code in Marcel the same way you would in Java.

Define comments like you would in Java. // ... for a single line comment, and /* ... */ for a multi-line comment

// this function does stuff
doStuff()

/* this function
  does some
  other stuff
 */
doOtherStuff()

Identifiers

An identifier is a name that you can use to define a class, field, a function or variable.

An identifier must follows all the below rules

  • it must start with a letter ('a' to 'z' , and 'A' to 'Z'), or an underscore
  • the following characters can be a letter, an underscore or a number

Class identifiers

To reference a class, you need tp add the .class suffix, like in Java. But note that you can only reference simple name of classes, this means that you need to import it first.

import java.util.concurrent.Callable
println(Callable.class)

println(Object.class)

Variables

Declare variables

Marcel variable declarations are the same as Java's

int a = 2
Object o = new Object()

list<int> l = [1, 2] // collection of primivites

Multiple declarations

Marcel supports multiple assignments in one statement

def (int a, String b, Object c) = [1, "2", new Object()]

def (int d, String e, Object f) = functionReturningAnArrayOrList()

Note that if the array/list is shorter than the number of variable declared, this will lead to a runtime error


Sometimes you might want to ignore a specific item of a list, You can use the _ identifier to let the compiler know that.

E.g.

def (_, String world) = ("hello world" =~ /hello (world)/).groups()

Variable assignments

Just use = to assign values to defined variables

int a = 2

a = 3

Properties

Marcel allows to access getters and setters as properties.

Suppose you have the below class

class Foo {
  private int bar
  
  fun getBar() {
    return this.@bar
  }
  
  fun setBar(int bar) {
    this.@bar = bar
  }
}

You could call these getBar/setBar methods using the property syntax. The @bar notation is the direct field access operator, make sure to reference the Java class's field.

Foo foo = new Foo()

foo.bar = 5 // will actully call foo.setBar(5)
println(foo.bar) // will actually call foo.getBar()

Automatic casting

Variable assignments are automatically casted when needed.

Optional o = Optional.of(123)
Integer myInteger = o.get() // returns Object cast into an Integer 
int myInt = o.get() // returns Object cast into an Integer then into an int

This can be useful as Marcel doesn't support generic types.

Types

In this section we'll explore some common Marcel types.

Java Types

Marcel is a JVM language, therefore you can use any classes defined in the JDK.

Generic Types

Marcel doesn't support generic types except for collections of primitive (which technically aren't really generic). You can use generic classes but cannot specify generic types when using them. it's a conscious choice made to get rid of some complexity while developing the compiler and also because Java always casts at runtime anyway.

Primitives

Marcel supports the following Java primitives

  • void
  • boolean (bool)
  • byte
  • short
  • int
  • long
  • float
  • double
  • char
  • byte

Literal Numbers

Marcel supports almost all Java primitives. The number primitive literals are the same as in Java

// primitive types
byte  b = 1
short s = 2
int   i = 3
long  l = 4l
float f = 5f
double d = 6d

Binary representation

You can also create numbers using their binary representation with the 0b prefix

int i = 0b10
long l = 0b11l

Hexadecimal representation

You can also create numbers using their hexadecimal representation with the 0x prefix

int i = 0x5
long l = 0x5l

boolean

You can create booleans using the true or false keyword.

bool b = true

char

Use the backtick (`) to create primitive characters. Only one character must be specified between the two backticks

char c = `A`

Escaped characters

Use backslash to escape 'special' characters within strings/characters. Here is the list of escaped characters

escaped characterrepresented value
\bbackspace
\nnewline
\rcarriage return
\ttabulation
\\backslash
\'single quotes (useful in simple strings)
\"double quotes (useful in interpolated strings)
\`backtick (useful in character strings)

Objects

Any non-null value that is not a primitive is an Object.

As specified before, Marcel is a JVM language, therefore you can use any classes defined in the JDK.

You can also define classes in Marcel

String

The Java String as you know it. There are different ways to create strings in Marcel

Simple strings

You can use the single quote character (') to create strings

'Hello world!'

Interpolated strings

You can use the double quote character (") to create strings resolving variables

"$firstName $lastName is $age years old"

If you need to access a property, use the brackets

"${person.firstName} ${person.lastName} is ${person.age} years old"

Pattern strings

You can instantiate Patterns using backslash strings. These strings are reserved for pattern only.

The backslash is not considered as an escape, except for the backlash character (which would be escaped as \/).

r/some \w+/

Note that such strings doesn't resolve variables. If you want to construct a Pattern while resolving Strings, you could just call the Pattern.compile(String) method with an interpolated string.


It is good practise to end such regexes with a semi-colon (;) character, to make it clear to the compiler that what follows is not a regex flag (we'll talk about that just after) but a 'real' identifier.

E.g.

Pattern pattern = r/myPattern/; // without the semi-colon, Marcel would think that 'println' characters are regex flags
println(pattern)

Pattern flags

You can also specify flags by adding a suffix at the end of your regex String.

Pattern pattern = r/myPattern/iu; // you can specify many flags at once
println(pattern)

Here is the list of flags (you can see the doc of each flag in the Javadoc of the Pattern's class).

characterJava PatternFlag
dUNIX_LINES
iCASE_INSENSITIVE
xCOMMENTS
mMULTILINE
lLITERAL
sDOTALL
uUNICODE_CASE
cCANON_EQ
UUNICODE_CHARACTER_CLASS

Arrays

Marcel supports Java arrays and has a syntax to specify array values.

Create arrays with the square brackets.

int[] ints = [1, 2, 3, 4]

Note that you can also use this syntax to create collections.

Collections

TODO

Collections of Primitives

Marcel allows to use collections with primitive elements. Such collections will not box all your primitives into their related Object class (e.g. store an int into an Integer). The elements will be stored in an array of primitives.

Iterating over such collections will only use primitives, no (un)boxing will be done.

Let's learn by example

Lists

list<int> list = [1, 2, 3, 4]

println(list[1])

list[1] = 1
println(list[1])

Here, we're declaring a list<int>. This type isn't actually generic, it is in fact an IntList (you can see this class in the marcel stdlib), and the literal array will be converted into a IntArrayList (a int list that store elements in an int array).

Here is the list of all list of primitives supported

  • list<int> -> IntList
  • list<long> -> LongList
  • list<float> -> FloatList
  • list<double> -> DoubleList
  • list<char> -> CharacterList

Sets

You can do the same with sets

set<int> mySet = [1, 2, 3, 3] // will actually contain just 1, 2 and 3

Here is the list of all set of primitives supported

  • set<int> -> IntSet
  • set<long> -> LongSet
  • set<float> -> FloatSet
  • set<double> -> DoubleSet
  • set<char> -> CharacterSet

Maps

Square brackets can also be used to define maps

Map map = [1.3: "1", 1.4: "2", "myStringKey": "myStringValue", 
           myLiteralKey: myRefValue, (myRefKey): myRefValue]

Note that myLiteralKey is actually a String key, it doesn't refer to a variable (like in Groovy). If you want to reference a variable as a key, put it between parenthesis, like it is done for (myRefKey).

Iterating over maps

To iterate over maps you can use the below syntax

Map map = [(1): "some", (2): "another"]
for ((int key, String value) in map) {
  println("$key -> $value")
}

Ranges

A Range represents the list of discrete items between some starting (or from) value and working towards some ending (or to) value. It may be reversed (e.g. from 10 to 1).

Marcel provides IntRange and LongRange

for (int i in 0..<5) println(i)

for (long i in 10l..1l) println(i)

You can create int (and long) ranges

0..10 // 0 (inclusive) to 10 (inclusive)
0<..10 // 0 (exclusive) to 10 (inclusive)
0..<10 // 0 (inclusive) to 10 (exclusive)
0<..<10 // 0 (exclusive) to 10 (exclusive)

Ranges also work in reverse order

10..0 // 10 (inclusive) to 0 (inclusive)
10>..0 // 10 (exclusive) to 0 (inclusive)
10..>0 // 10 (inclusive) to 0 (exclusive)
10>..>0 // 10 (exclusive) to 0 (exclusive)

Ranges work with all kinds of int/long expressions

int start = computeStart()
int end = computeEnd()

for (int i in start..(end - 1)) println(i)

Dynamic Objects

Dynamic objects a little dynamic feature in Marcel. They allow you to evaluate dynamically property/operator calls instead of at compile time.

E.g.

dynobj o = 1
println(o[1]) // will throw MissingMethodException at runtime, instead of a semantic error at compile time

But the following code will run without throwing any exception

dynobj o = DynamicObject.of([1, 2, 3] as list<int>)
println(o[1]) // will print 2

DynamicObject could potentially handle dynamic method calls and handle properties, but this is not done as Marcel is not designed to be a dynamic language in the first place.

Control flows

In this chapter you'll explore all control flows thar Marcels offers, in particular how to perform

  • if/else statements
  • for loops
  • while loops
  • switch
  • try/catch/finally

Marcel truth

A truthy value is a value that is considered to be true for an if, or a while.

A falsey value is a value that is considered false in those places.

The only falsey values are the following:

  • false
  • null
  • a Boolean instance whose value is false
  • Optional.empty(), OptionalInt.empty(), OptionalDouble.empty(), OptionalLong.empty()
  • An empty collection
  • An empty array
  • An empty Map
  • An empty String
  • A Matcher who's find() method would return false
  • A File who's exists() method would return false
  • An object implementing MarcelTruth who's isTruthy() method would return false

Any other value is truthy.

If expression

If statements are similar as Java's, but you can provide any expression in them. If the provided expression is not a boolean, the Marcel truth will decide if your expression is true or not.

if (a == 1) {
  println("a is 1")
} else if (a == 2) {
  println("a is 2")
} else {
  println("a is not 1 and not 2")
}

if variable declaration

The marcel truth allows you to declare variable in an if condition, and execute the code block if the variable is truthy

if (Something result = fetchSomething()) {
  println("Fetched $result")
}

You can also unbox Optional values such as in the below example

// assuming getOptionalInteger() returns an Optional
if (Integer result = getOptionalInteger()) {
  println(result)
}

While loops

While loops are also similar as Java's. You can also provide any expression in them as you would in a Marcel if. If the provided expression is not a boolean, the Marcel truth will decide if your expression is true or not.

int i = 0
while (i < 10) {
  println(i++)
}

while variable declaration

The marcel truth allows you to declare variable in an while condition, and execute the code block if the variable is truthy

while (String line = reader.readLine()) println(line)

// outside the loop, this line variable doesn't exist anymore

Do while

You can also perform do-while instructions, which will always execute at least one the do statement and then check the condition. If the condition is true the do statement is executed again.

int i = 15
do {
  println(i++)
} while (i < 10)

The above code will only print 15.

Do

You can specify a do instruction without a while instruction at the end. It will just execute the do statement once. This can be useful to create inner scopes, in which you can create all the local variables you want, as they won't be accessible outside the do scope


int result = 5
do {
  int variable = 1
  int anotherVariable = 2
  int anotherOtherVariable = 3
  int anotherOne = 5
  result = variable + anotherVariable + anotherOtherVariable + anotherOne
}

// now only result variable exists

For loops

There are different ways to iterate over elements

For i

The Java for i is compatible with Marcel

for (int i = 0; i < 10; i++) {
  println(i)
}

For in

The in keyword allows to iterate over values in an array, any objects implementing Iterable (including all Collections) or Iterator.

int[] ints = getInts()
for (int i in ints) {
  println(i)
}

Marcel also have a Ranges, allowing you to iterate with the below syntax

// inclusive range
for (int i in 0..9) {
  println(i)
}

// exclusive range
for (int i in 0..<10) {
  println(i)
}

// also work in reverse orde
for (int i in 9..0) {
  println(i)
}

// exclusive range
for (int i in 10>..0) {
  println(i)
}

For in map

You can also iterate over a Map entries using the below syntax

Map m = [foo: 1, bar: 2]
for ((String key, int value) in m) println("$key=$value")

Switch and When

Marcel defines conditional control flows that can return values. Note that in the below control flows described, you can't re-assign a value to local variables created outside the switch/when scope.

When

when {
  string == "foo" -> 2
  string == "bar" -> {
    doSomeStuff()
    3
  }
  otherCondition() -> 4
}

Each when branch consists of a condition, and a statement. If the given condition is true, the corresponding statement will be executed.

Whens are very similar to if/elseif/else control flow, but they allow you to return values. They are useful to assign variables, or returning values in functions.

In the above example, you can notice that a default case is missing. If no conditions matched, the when will return null.

This means that you must always specify an else branch for whens returning primitive types, as they cannot be null.

int myInt = when {
  string == "foo" -> 2
  string == "bar" -> {
    doSomeStuff()
    3
  }
  string == someString() -> 4
  otherCondition() -> 5
  else -> 5
}

Switch

Switch are very similar to whens. Every switch can be translated to a when (but the other way around is not true).

In switches, you compare an expression against multiple values. Based on the above when example, we could do the following switch

switch (string) {
  "foo" -> 2
  "bar" -> {
    doSomeStuff()
    3
  }
  someString() -> 4
}

You'll notice that we couldn't translate the when condition otherCondition(), this is because it isn't a comparison against the switched expression.

Each switch branch consists of a value, and a statement. If the provided switch expression matches the branch's expression, the corresponding statement will be executed.

Switches also have an else, that is required when returning a primitive

int myInt = switch (string) {
  "foo" -> 2
  "bar" -> {
    doSomeStuff()
    3
  }
  someString() -> 4
  else -> 5
}

Access the switched expression

The provided expression can be accessed in the switch branches using the implicit variable it.

int myInt = switch (computeSomeInt()) {
  1 -> it + 1
  else ->  it + 4
}

Exception Handling

Throw Exception

In Marcel you can throw Exceptions (or any Throwable) like in Java

throw new RuntimeException("Error Message")

You are not forced to catch checked Exceptions in Marcel.

Try/Catch/Finally

Exception handling is very similar as Java's.

try {
  Object a = null
  println(a.hashCode())
  println("Successfully tried")
} catch (IOException|NullPointerException e) {
    println("Caught exception")
} finally {
  println("finally")
}

The above code will print

Caught exception
Finally

Try with resources

Try with resources is like in Java. You can declare Variables as resources and they will be properly closed automatically by the compiler.

try (BufferedReader reader = Files.reader('input.txt')) {
  println(reader.readLine())
} catch (IOException e) {
  e.printStackTrace()
}

Program structure

Package

A Marcel source file can have a package. It is optional but if it is specified, it must be the first instruction in the file (excluding comments)

package my.package

Imports

Then, some imports can follow. You can consult the default imported class/package here

Class

You can define classes like in Java.

Functions

Classes can have functions

Fields

Classes can also have fields

Imports

Marcel's imports are very similar to Java's.

Class import

Such imports are like Java's

import java.text.SimpleDateFormat

But Marcel adds the capability to import a class as a given name. All references to the given name will be replaced by the actual class imported when compiling

import java.text.SimpleDateFormat as SDF

SDF sdf = someSdf()

Wildcard imports

Again, just like Java

import java.text.*

Static imports

Yup, like in Java

import static org.junit.jupiter.api.Assertions.assertEquals

Default imports

Marcel import by default all the following packages

  • java.lang.*
  • java.util.*
  • java.io.*
  • marcel.lang.*

Visibility and Access

In marcel, there are 4 kinds of visibility.

  • public -> which refers to Java's public visibility. Your class/method/field may be accessible from any package
  • protected -> which refers to Java's protected visibility. Your class/method/field may only be accessible from other classes in the same package or inheriting your class
  • internal -> which refers to Java's package-private visibility. Your class/method/field may only be accessible from classes in the same package
  • private -> Your method may be accessible only from the class it was defined in

The default visibility is public (meaning that when it isn't specified, the class/method/field will be considered as public)

Access

Class/method/fields access should be specified in the below order.

  1. public/protected/internal/private (or nothing, which would default to public visibility)
  2. static (Optional. only if you want your member to be static)
  3. final (Optional. only if you want your member to be final)

Functions

Use the fun keyword to define functions

fun int sum(int a, int b) {
  return a + b
}

protected fun void foo() {
  // do nothing
}

As shown in the above example, a function define has the following structures

  1. starts with the visibility which is optional and defaults to public.(you can also define static function with the static keyword)
  2. the fun keyword
  3. the return type
  4. the function's name
  5. the list of your function's parameters. The parameter's type first, and then the parameter's name.

If your function only contains one statement/expression, you can specify it with the below syntax

fun int sum(int a, int b) -> a + b

protected fun void foo() -> println("Did nothing")

Function Visibility

You can specify your function's visibility before the fun keyword

private fun foo() {
}

Function Calls

Function calls are no different from in any other language

int result = sum(1, 2)

Cast Results

Marcel has a diamond operator for function calls which is different from Java's. It casts the result of the function to the specified type.

Foo otherResult = compute<Foo>()

This above example isn't really useful as Marcel automatically cast variable assignments when needed but this feature can be useful when chaining function calls

Optional opt = Optional.of(new Foo())
// assuming computeObject() and result return Object in their declaration
Bar result = opt.get<Foo>().computeObject<Bar>()

But note that this is useless if the function/property already returns the specified type in their declaration.

Named Parameters Call

You can also call a function by specifying its parameters by name. When doing so, the order in which you specify them doesn't matter.

Such calls can also start with positional arguments.

Following on our sum() example:

int result = sum(b: 2, a: 1) // equivalent to sum(1, 2)
int otherResult = sum(1, b: 0) // equivalent to sum(1, 0)

It works the same with constructors. Here are some examples below.

class B {
  int i
  int j
  
  constructor(this.i, this.j)

}
class C {
  int a
  int b
}

B b = new B(i: 1, j: 2) // will call new B(i, j)
C c = new C(b: 2, a: 1) // will call new C(a, b)
int result = sum(2, b: 1) // equivalent to sum(2, 1)

int otherResult = sum(a: 2, 1) // ERROR, positional argument is not at the start 

Note that you can only used named parameters call for functions of Marcel-compiled classes, because Java doesn't keep method parameter names available at runtime by default.

Parameter default value

Function parameters can have default values, which are used when you skip the corresponding argument. These can be useful especially with named parameters function calls.

fun int sum(int a = 0, int b = 8) {
return a + b
}

sum(a: 2) // 2 + 8
sum(b: 5) // 0 + 8
sum(a: 2, b: 5) // 2 + 5

You can specify any expression from a static context (this means you can't call/use non static functions/fields from the class your method is defined).

These default parameter values are kept after compilation so you can also benefit them from other Marcel libraries.

Optional parenthesis

When calling a function with at least one parameter, you can omit the parenthesis.

Here are some examples

int result = sum 1, 2
println result

doSomethingWithAnIntAndALambda 1, { /* my lambda */ }

Classes

You can define classes using the class keyword

class Foo {

}

Extending/Implementing classes/interfaces

The syntax is like Java's

class Foo extends Object implements List<Integer> {

}

Class visibility

You can specify your class's visibility before the class keyword

public class Foo {

}

Class functions

See the functions section to see how to define functions

Class fields

You can define class fields like you would in Java

class Foo {
  private int a;
  double b = 3
  Object c;
}

Constructors

You can use the keyword constructor to define constructors. The definition is similar to a function

class Foo {
  int bar
  String zoo
  
  constructor(int bar, String zoo) {
    this.bar = bar
    this.zoo = zoo
  }
}

Constructors where you just want to assign values to your fields are common use-cases. Marcel has a syntax allowing you to write such constructors with a less verbose code.

class Foo {
  int bar
  String zoo
  
  constructor(this.bar, this.zoo)
}

We didn't even specify a function block, but you can specify one if you want. The first statements of your class will be the field assignments (after the super() call of course).

Calling constructors

You can call specific super and this constructors.

class A {
  int foo
  constructor(this.foo)
}

class B extends A {
  int bar
  constructor(int foo, this.bar): super(foo) {
    println("Yahoo")  
  }
  
  constructor(this.bar): this(0, bar)

}

Write Scripts

Scripts don't need a main() function. You can just start writing statements of your script directly, without wrapping them in a method. Script can be executed easily with MarCL.


You can also define functions in your scripts.

E.g.

println(fibonacci(10))

@cached
fun int fibonacci(int n) -> switch (n) {
  0, 1 -> n
  else -> fibonacci(n - 1) + fibonacci(n - 2)
}

Local Variables

To declare a local variable in a script, simply declare it as you would in a function's body.

int a = 2
int b

Fields

To declare a class field for your script, you must explicitly provide its visibility, otherwise it will be considered as a local variable.

E.g.

internal int myField1 = 2
protected myfield2

Note that global variables are a lot like field variables so you don't need to use both. Just use global variables or field variables based on your preferences.

Classes

You can also define classes in a script, but note that such classes will not be an inner class of your script. They will be top-level classes.

Annotations

Annotations are a lot like Java's. Use the @MyAnnotation syntax to annotate a class, field or method parameter.

@MyAnnotation
class MyClass {
  @MyAnnotation
  int myField

  fun int myMethod(@MyAnnotation Integer someInt) {
    return someInt + myField
  }
}

Annotation attributes

You can specify attributes like in Java. If your annotation only has one attribute, with the name value() You can just type your attribute value between parentheses.

@MyAnnotation(1)

Or you can specify the attribute name like in the below example

@MyAnnotation(value = 1)

If you have multiple attributes, separate them with a comma

@MyAnnotation(value1 = 1, value2 = 3)

Enum attributes

When specifying enum attributes you just have to specify the enum's name, without its class

@MyAnnotation(timeUnit = MILLISECONDS)

Operators

Marcel provides arithmetic operators to work with numbers (+, -, /, * and soonish %) but there are also operators that can be used on specific types.

We will explore all of them in this section

Plus operator (+)

The plus operator is used for arithmetic and concatenating Strings, but in Marcel it also has other uses.

Add collections

You can use + to add Collections. The two operand will not be modified. A new collection will be created with both operand added to it.

It works well with lists


list<int> myList1 = [1, 2, 3]
list<int> myList2 = [4, 5, 6]

list<int> myList3 = mySet1 + mySet2
println(myList3) // [1, 2, 3, 4, 5, 6]

and sets


set<int> mySet1 = [1, 2, 3]
set<int> mySet2 = [3, 4, 5]

set<int> mySetUnion = mySet1 + mySet2
mySetUnion(myList3) // [1, 2, 3, 4, 5]

You can also add different kind of collections. The type of the returned collection will be the same as the first operand

set<int> newSet = mySet1 + myList1
list<int> newList = myList1 + mySet1

Minus operator (-)

The minus operator is used for arithmetic but in Marcel it also has other uses.

Add collections

You can use - to add Collections. The two operand will not be modified. A new collection will be created with the elements of the first operand having removed all elements from the second operand if any.

It works well with lists


list<int> myList1 = [1, 2, 3]
list<int> myList2 = [3, 4, 5]

list<int> myList3 = mySet1 - mySet2
println(myList3) // [1, 2]

and sets


set<int> mySet1 = [1, 2, 3]
set<int> mySet2 = [3, 4, 5]

set<int> mySetUnion = mySet1 - mySet2
println(mySetUnion) // [1, 2]

You can also add different kind of collections. The type of the returned collection will be the same as the first operand

set<int> newSet = mySet1 - myList1
list<int> newList = myList1 - mySet1

Left Shift operator (<<)

Left shift operator allows to add elements to collections

list<int> a = [1, 2, 3]

a << 2

Comparison Operators

Equal (==)

Unlike in Java == operator will do its best to compare the 2 operands by value. This operator is null safe, meaning that if one of the operand is null, it won't throw a NullPointerException.

This operator works as described below

  • if both operand are primitives, Java-like == will be performed
  • if both operand are primitive arrays, Arrays.equals() will be used to compare the values
  • if both operand are object arrays, Objects.deepEquals() will be used to compare the values
  • otherwise it will call Objects.equals()

This logic is applied at compile-time, meaning you won't have a runtime overhead because the program would have to check types.

  • if at least one of the two operand is an Object, the other operand is casted as an object if needed and the Marcel == is applied.

Not Equal (!=)

The not equal is the negation of the Marcel's ==

LT, LOE, GT, GOE (<, <= ,>, >=)

These operators works like in Java for primitive types. For object types, Marcel will check at compile-time if the first operand has a compareTo() method and apply it on the second operand. The result of the compareTo will be used to apply the given comparison.

Is Same (===)

This operator is the Java's == operator for Objects. It will check if the two operand are the same instance, or are both null. (Note that it can't be used on primitives).

Is Not Same (!==)

It is the negation of the Is Same operator.

As (smart casting)

The as keyword allows to smart cast variables to a provided type.

Smart casting

Smart casting is like an enhanced Java cast. It can cast objects like a Java cast would, but it can also convert the object to make it fit the target type.

Collections smart casting

The smart cast can transform arrays into (primitive) lists/sets

int[] array = [1, 2, 3]

list<int> intList = array as list<int>
set<long> longSet = [1l, 2l, 3l] as set<long>

Boolean (truthy) smart casting

You can smart cast any value to a boolean. The value of the boolean will be determined based on the Marcel truth.

Dynamic object smart casting

Any type can become a dynamic object

dynobj obj = 1 as dynobj

(Java) casting

To perform a simple cast, you can use the function cast with the diamond operator, provided specifically for this use-case

int a = 1

Integer b = a as Integer
Number c = b as Number
Long d = c as Long // will fail as a is not an instance of Long

Instance Of (type checking)

The instanceof keyword allows to verify if an Object variable is an instance of the provided type.

It cannot be used on primitive variables, and will always return false on null variables.

Examples

Integer a = 1

println(a instanceof Integer) // true
println(a instanceof Number) // true
println(a instanceof Long) // false

Not Instance Of

To check the opposite, use !instanceof

Integer a = 1

println(a !instanceof Integer) // false
println(a !instanceof Number) // false
println(a !instanceof Long) // true

Indexed Access Operator (expr[index])

This operator is usually (or at least in Java) reserved for arrays, but in Marcel you can also use it to access lists. You can get/set items of your list using the same syntax as for an array


list<int> list = [1, 2, 3]

println(list[0])

list[1] = 4

You can also define your own accesses for custom types

Safe indexed access (getAtSafe)

Similarly to safe navigation, you can access elements of list/arrays

println(list?[5]) // will print null

This operator checks that the index provided is within the list/array's bounds (0 <= index < length)

You can also set elements safely with the putAtSafe operator

list<int> = [1, 2, 3]

list?[1] = 5 // will actually set the value
list?[10] = 4 // will not set the value as the index is not within bounds

Safe Navigation Operator (?.)

The safe navigation operator is used to access a property of an object that might be null, without getting a NullPointerException.

It is a simple syntax allowing you to simple code. In Java, you could code

Foo foo = getFoo()
Bar bar = foo != null ? foo.getBar() : null

In Marcel, you would code

Foo foo = getFoo()
Bar bar = foo?.bar // Marcel recognizes getters and translates '.bar' into '.getBar()' at compilation

Ternary operator ( ? : )

Marcel supports ternary operator like in Java. This operator have 3 operands:

  1. the condition expression
  2. the 'true' expression
  3. the 'false' expression.

This operator evaluates the condition. It can be any kind of expression, as the Marcel truth will determine if the expression is truthy or not, for non-boolean expressions.

If the condition expression is truthy, the 'true' expression will be evaluated, otherwise it will be the 'false' expression.

E.g.

int temperature = isSunny() ? 21 : -5
println(temperature)

fun bool isSunny() -> return true

This script will print the value 21

Let's take a look at another example.

Integer input = null
Integer a = input ? input : 34
println(a)

This script will print the value 34

Note that this last example can be simplified using the Elvis operator

Elvis operator

The Elvis operator is just a simplified ternary operator in which the condition expression and the 'true' expression are the same.

You could translate the above example using the below code

Integer input = null
Integer a = input ?: 34
println(a)

Direct field access @fieldName

This operator allows to make sure to reference a class field, and not a getter/setter.

E.g.

class Foo {
 int bar
  
  fun getBar() {
    return this.@bar
  }
  
  fun setBar(int bar) {
    this.@bar = bar
  }
}

Foo foo = new Foo()

In the above class, calling foo.@bar or foo.@bar = 4 would make sure to actually use the field, and not the getter/setter.

Define custom operators

You can define operators in a very similar way as groovy's.

Each operator is associated to a function. To define an operator for a given type.

Here is the table of functions to define for each operator

Define operators from extensions

OperatorMethod
a + ba.plus(b)
a - ba.minus(b)
a * ba.multiply(b)
a % ba.mod(b)
a / ba.div(b)
a << ba.leftShift(b)
a >> ba.rightShift(b)
a[b]a.getAt(b)
a?[b]a.getAtSafe(b)
a[b, c, d]a.getAt(b, c, d)
a[b] = ca.putAt(b, c)
a?[b] = ca.putAtSafe(b, c)

Iterable Operations

Iterable operations provide a special syntax to perform common operations of a daily programmer's life on Iterables.

Actually those operations don't only work with Iterables, but also with arrays and CharSequence (e.g. String).

Map and/or Filter

Map

You can create a new collection resulting of the mapping of another.

list<int> list = [1, 2, 3, 4]
list<int> list2 = [for int i in list -> i + 1] // [2, 3, 4, 5]

Filter

Filtering is also possible using a similar syntax.

list<int> list = [1, 2, 3, 4]
list<int> list2 = [for int i in list if i <= 2] // [1, 2]

Map and Filter

You can do both in one operation.

list<int> list = [1, 2, 3, 4]
list<float> list2 = [for int i in list -> i + 0.1f if i <= 2] // [1.1f, 2.2f]

Casting

All the above operations can return Lists, primitive Lists, Sets, or primitive Sets. The type is usually guessed by the compiler when possible (e.g. looking at the type of the variable you're trying to set), but you can explicitly specify the wanted type using the as operator.

list<int> list = [1, 2, 3, 4]
set<int> list2 = [for int i in list -> i % 2] // [0, 1]
println([for int i in list -> Optional.of(i % 2)] as Set) // [Optional(0), Optional(1)]

Any, All

Checking if any/all elements of an Iterable, CharSequence or array all matches a given predicates is possible using the below syntax.

Any

list<int> list = [1, 2, 3, 4]
println(when int a in list |> a >= 3) // true

The |> arrow is used to check if at least one element matches the predicate.

All

list<int> list = [1, 2, 3, 4]
println(when int a in list &> a >= 3) // false

The &> arrow is used to check if all elements matches the predicate.

Negations

You can also negate those conditions using the !when keyword.

println(!when int a in list |> a >= 3) // false
println(!when int a in list &> a >= 3) // true

Complex boolean expressions

To use properly the above described operations in boolean expressions, wrap them with the parenthesis to avoid any ambiguity

if ((when int a in list &> a >= 3) && somethingElse) {
  doAllTheThings()
}

Find

This iterable operation allows to find an element on an Iterable, CharSequence or array and return it, or null. As the element may not be found, this operator always return an object, even for collections of primitive (e.g. Integer for a list<int>).

list<int> list = [1, 2, 3, 4]
println(when int a in list -> a % 2 == 0) // 2
println(when int a in list -> a % 2 == 5) // null

Chain operations

You can chain multiple iterable operations in a same expression using the right shift (>>) operator.

list<int> list = [1, 2, 3, 4]
println(list >> when int a -> a % 2 == 0)
println([for int a in list -> a + 3] >> when int a -> a % 2 == 0)

In those example you can see that we omit the in something part of the operations, this is because the left operand of the >> operator is used instead.

So the above code is equivalent of the below code.

list<int> list = [1, 2, 3, 4]
println(when int a in list -> a % 2 == 0)
println(when int a in [for int a in list -> a + 3] -> a % 2 == 0)

But this piece of code is hard to read, right? That is why the >> is here.

Lambdas

Marcel supports lambda expressions, but note that they are not compiled to Java lambdas, they are compiled to anonymous classes instead.

Lambda are declared like Kotlin's, and Groovy closures. They can be used for any functional interfaces, such as Runnable, Supplier...

Lambdas with no parameters

Lambda0 l0 = {
  
}

Lambdas with 1 parameter

If you don't specify any parameter, an implicit Object it parameter will be declared.

Lambda0 l1 = {
  println(it)
}

Or you can explicitly declare it yourself, specifying the type

Lambda1 l1 = { Integer p0 ->
  println(p0)
}

Lambdas with 2+ parameter

You have to declare explicitly all parameters, separated by a comma

Lambda3 l3 = { Integer foo, Long bar, String zoo ->
  // do something
}

Extension classes

Extension classes allows you to add methods to an existing class.

How to declare

An extension class is declared like a regular class, but with the keyword extension. You'll also need to specify which class your are extending.

extension class MyExtension for Integer {
}

Define instance methods

Define methods as you would if you were in the class you are extending (not talking about inheritance).

extension class MyExtension for Integer {
  
  fun int next() {
    return this + 1
  }

  fun float foo() {
    return floatValue() + 2f * next()
  }

}

As you can see in the above example, you can also call methods of the extended class, and other extensions methods you defined.

Define static methods

This works the same as instance methods. Define your static method as if you were in the extended class.

extension class MyExtension for Integer {
  
  static fun int zoo() {
    return 1
  }

}

How to use

Import your extension class with the extension keyword.

// another file
import extension MyExtension

Integer a = 1
println(a.next())
println(Integer.zoo())

Note that you can also use an extension in the same file it was declared in. In such case, you don't need to specify the import

extension class MyExtension for Integer {
   fun int next() {
      return this + 1
    }

    fun float foo() {
      return floatValue() + 2f * next()
    }

   static fun int zoo() {
      return 1
    }
}

Integer a = 1
println(a.next())
println(Integer.zoo())

Metaprogramming in Marcel

Marcel offers some metaprogramming features, allowing you to write code that generates code.

Such code is generating while compiling it. As Marcel is a static language, it only supports compile-time metaprogramming and has no runtime metaprogramming features.

Compile-time metaprogramming with Syntax Tree Transformations

Syntax Tree Transformations can modify the representation of your source code before converting it into Java bytecode.

This process can alter the Concrete Syntax Tree (CST) and/or the Abstract Syntax Tree (AST) of your program.

This concept is similar as Groovy's AST transformations, except that in Marcel you can also transform the CST, and the AST transformation occurs after the semantic analysis.

This SyntaxTreeTransformation interface specifies the transformation of a CST/AST node and operates in 2 steps occurring in different phases of the compilation process (3 if you count the initialization).

How Syntax Tree Transformations operate

After the parsing of your code which outputs the Concrete Syntax Tree (CST), and before performing the semantic analysis which would generate the Abstract Syntax Tree (AST), all symbols (classes, methods, fields) are defined.

Then occurs the first step of an Syntax Tree transformation: the symbol definition transformation. A Syntax Tree transformation can alter the definition of symbols (e.g. make your class implement an interface, or add a field to a class, modify a method signature...). This step only affects the definition of the symbols. It tells the compiler (for example) that "This class also implements the Foo interface" or "This class has a field bar of type int", and the compiler will just have to trust it, especially when performing the semantic analysis.

In this step you can also modify the Concrete Syntax Tree.

After all the Syntax Tree transformations completed their first step, the semantic analysis is performed.

Finally, comes the second step of Syntax Tree transformations: the AST transformation. This step can alter in many ways the AST, but it must alter it carefully as not all semantic checks are performed while doing so.

The modifications made on the AST must also be coherent with the previous symbol definition transformations. E.g. if we defined a new method in the first step, we must create and add a new valid method node with the same signature in the AST, in the second step.

Meta-annotations (metaprogramming with annotations)

AST transformations can be specified on annotations. Doing so allows to perform specific transformations when annotating a given class, field, method and/or field.

Marcel's standard library provides many annotations useful to avoid writing boilerplate code. Some of them are similar as the one you could find in Lombok.

Meta-annotations from the Marcel's standard-library are all lowercase (even the first letter), this is how you can differentiate them from other (non-meta) annotations.

@stringify

This annotation is similar to Lombok's @ToString annotation. It auto-generates a toString() method for your class, based on your class's members.

The generated toString method will put all fields of the class with their values in the generated String.

@stringify
class Foo {
 String bar = "myBar"
}

println(new Foo())

This script would print Foo(bar=myBar).

Include getters

The getters are not included in the generated String by default. To change this behaviour, you can use the flag includeGetters=true.

@stringify(includeGetters=true)
class Foo {
 String bar = "myBar"
 
 fun int getZoo() -> 5 
}

println(new Foo())

This script would print Foo(bar=myBar, zoo=5).

Exclude particular fields/methods

You can use the annotation @stringify.Exclude to exclude a particular field or getter.

@stringify(includeGetters=true)
class Foo {
 int i = 1

 @stringify.Exclude
 String b = "srsr"

 @stringify.Exclude
 fun String getFoo() -> "foo"

 fun String getBar() -> "bar"
}

This script would print A(i=1, bar=bar).

@comparable

This makes your class implement Comparable interface. The comparison will be made based on your class's field, in the order in which they were defined.

For example take a look at the below class

@comparable
class Foo {
  int a
  String b
  double c
}

When comparing 2 Foo instances, we'll start by comparing the fields a. If they are not the same (one is greater/lower than the other), we'll stop the comparison here as we already can determine which Foo instance is greater than the other. If both a fields hold the same value, we'll continue the comparison with b, and so on and so on...

Include getters

The getters are not included in the generated Comparison by default. To change this behaviour, you can use the flag includeGetters=true.

Exclude particular fields/methods

You can use the annotation @comparable.Exclude to exclude a particular field or getter.

@comparable(includeGetters=true)
class Foo {
 int i = 1

 @comparable.Exclude
 String b = "srsr"

 @comparable.Exclude
 fun String getFoo() -> "foo"

 fun String getBar() -> "bar"
}

@data

This annotation is similar to Lombok's @Data annotation. It auto-generates the equals(), hashCode() and toString() method for your class, based on your class's members.

The

You can also make your class implement Comparable by providing the comparable=true flag

@data
class Foo {
  int a = 2
  String b = "b"
}

Generate an all args constructor

You can generate a constructor for your class using the withConstructor flag.

@data(withConstructor=true)
class Foo {
  int a
  String b
}
println(new Foo(1, "b"))

This code will generate the constructor constructor(this.a, this.b).

To omit a particular field for the generated constructor, annotate the field with @data.Exclude.

@data(withConstructor=true)
class Foo {
  int a
  @data.Exclude
  String b
}

println(new Foo(1))

This code will generate the constructor constructor(this.a).

Exclude particular fields/methods

You can use the @data.Exclude, @comparable.Exclude or @stringify.Exclude annotations to exclude properties from the sring representation,

@data.Exclude will exclude the property from both the toString(), and the equals(),hashCode() methods. @comparable.Exclude will only exclude the property for the equals(),hashCode() methods. @stringify.Exclude will only exclude the property for the toString() method.

@data
class Foo {
 @data.Exclude
 int i = 1

 @comparable.Exclude
 String b = "srsr"

 @comparable.Exclude
 fun String getFoo() -> "foo"

 fun String getBar() -> "bar"
}

@cached

This annotation allows to cache results of a method in order to prevent having to compute multiple times the result for a same input. It will generate a cache and make the annotated method use this cache. The cache key is produced with the method parameter(s).

If the value for the given input (method parameters) is in the cache, we just return it from the cache. If it isn't in it, we compute the value, put it in the cache and then return it.

A perfect example is a recursive implementation of the fibonacci suite. With high numbers, we may compute multiple times the value for a same input, making the operation really long. That's where @cached comes to save the day. No value for a same input will be computed more than once, this will allow to save a lot of time.

println(fibonacci(10))

@cached
fun int fibonacci(int n) -> switch (n) {
  0, 1 -> n
  else -> fibonacci(n - 1) + fibonacci(n - 2)
}

This script would print Foo(bar=myBar).

Caching also work with functions having many parameters

println(funnynacci(10, 15))

@cached
fun int funnynacci(int n, int m) -> switch (n) {
  0, 1 -> n + m
  else -> funnynacci(n - 1, m - 1) + funnynacci(n - 2, m - 1)
}

Thread-safe cache

By default, the cache is not thread safe and is backed by a HashMap.

You can set the threadSafe flag to true if you want to make the implementation thread-safe. The cache will then be backed by a ConcurrentHashMap.

println(fibonacci(10))

@cached(threadSafe=true)
fun int fibonacci(int n) -> switch (n) {
  0, 1 -> n
  else -> fibonacci(n - 1) + fibonacci(n - 2)
}

@lazy

This annotation is used to make a field lazy. The value of the field will only be computed when it is referenced, and not before.

Prerequisites

The annotated field must be of Object type (non-primitive) and must have an initial value specified.

E.g.

@lazy
Integer i = computeI()

fun int computeI() {
  initCount++
  return 1
}

How it is transformed

This meta-annotation will transform the code to make the annotated field lazy.

Using the above example, the code would be transformed as such

private Integer _i = null

fun getI() {
  if (_i == null) {
    _i = computeI()
  }
  return _i
}

fun int computeI() {
  initCount++
  return 1
}

As Marcel allows to access getters as properties, you can access this variable using the a syntax (or this.a), as if you were referencing the original field. You'll notice that in the above script, the value of i will never be computed, as the variable i wasn't referenced anywhere outside.

Now let's take a look at a full example.

@lazy
Integer i = computeI()

for (int _ in 1..3) println(i)

fun int computeI() {
  initCount++
  return 1
}

The output of the above script would be

Computing...
1
1
1

As the variable is lazy, the value of i will be computed at the first call of println(i), and the other calls will just use the already computed value.

Asynchronous programming in Marcel

This feature is currently under development and is therefore not available yet.

Marcel provides an async/await paradigm allowing to execute tasks in the backgrounds. In Marcel, asynchronous (AKA async) code will be executed on background threads (virtual threads if you JRE supports it).

Async functions

Async functions provide a way to write functions that are executed in the background when called

async fun int compute() {
  Thread.sleep(2000l)
  return 1
}

The actual return type of async functions are Futures (in above example a Future<Integer>).

Await

The await keyword allows to wait for the result of an asynchronous function.

async fun int computeInBackground() {
  int result = await doCompute()
  println(result)
}

Async context

It's important to know that async functions can only be executed in an async context., that is in an async function or in an async block (we'll get on that later).

E.g. the below code example wouldn't compile

async fun int doCompute() -> 1

fun void computeInBackground() {
  int result = await doCompute() // Compiler error: cannot call async function in a non async context
  println(result)
}

Async block

An async code block is a block in which you can perform async operations

async fun int doCompute() -> 1

async {
  int result = await(doCompute())
  result
}

async blocks will always wait (at the end) for all asynchronous tasks to complete. Although this is done automatically, you can also do it manually with the await function.

await

Await is (a set of) static methods that you can use in async contexts.

await()

Awaits for all asynchronous tasks to complete.

await(Future)

Awaits for a particular asynchronous task to complete.

await(Collection), await(Object[])

Awaits for a collection/array of asynchronous tasks to complete.

await(AwaitProgressListener)

Awaits using the provider lambda to listen to progress update. E.g.

async {
  doCompute()
  await { int completedTasks, int total -> 
    print("\rComputed $completedTasks out of $total configurations")
  }
}

Tutorials

In this section, you'll find all sort of tutorials explaining how to implement a given pattern (or play with a given feature) in Marcel.

Collections

In this section we'll explore ways to use Marcel Collections APIs enhancements

Literals

You can declare collections using the literal array syntax as shown below

list<int> ints = [1, 2, 3, 4]
List list = [1 new Object(), 1..2]


set<char> chars = [`A`, `C`, `B`, `D`]

Operate on Collections and Arrays

Collections and Arrays have useful functions allowing you to operate on them

map

This operator allows you to map each element of the collection to another.

list<int> ints = [1, 2, 3]
list<float> floats = ints.mapToFloat { it + 1f } // mapping to another collection of primitive
int[] intArray = [3, 4, 5, 6]
list<int> otherInts = intArray.mapToInt { it - 1 }

List list = ints.map { new Foo(list) }
// specified type explicitely
List list2 = list.map { Foo foo -> foo.bar }

Note that for collections of primitive you don't have to specify explicitly your parameter's type. But for non-primitive lists, as Marcel doesn't support generic types, you'll have to explicitly declare your lambda's arguments.

find, findAll

The find operator finds and return the first element of the collection/array matching the provided predicate (or null if no element matched).

findAll returns a list of all elements matching the given predicate

set<int> ints = [1, 2, 3]
int myInt = ints.find { it == 2 }
int[] intArray = [3, 4, 5, 6]
int myInt2 = intArray.find { it == 5 }
list<int> myInts = ints.findAll { it >= 2 }


List list = [1..1, 2..3, 3..4]
IntRange myRange = list.find { IntRange range -> range.contains(4) }
List myRanges = list.findAll { IntRange range -> range.contains(2) }

any, all, none

Those operations return booleans.

  • any returns true if at least one element of the collection/array matches the given predicate
  • all returns true if all elements of the collection/array matche the given predicate
  • none returns true if all elements of the collection/array does not match the given predicate

Regular Expressions (Pattern Matching)

Marcel's Pattern strings allows you to create pattern in a simple manner. When you add that with the find operator, matching with regular expression has never been this easy.

Pattern pattern = r/Hello (\w+)/; // semi-colon required because of pattern flags
Matcher matcher = "Hello you" =~ pattern
println(matcher.matches())

The above code tests is the String Hello you matches the pattern Hello (\w+).

Extract groups from a pattern

With Marcel's multiple variable declaration, you can extract matched groups in the following way

def (String wholeMatch, String groupMatch) = ("Hello you" =~ r/Hello (\w+)/).groups() // method from the Marcel Development Kit

In some case, you might not care about the whole match (you just want the groups you declared in your regex). If that's so you can ignore it like this

def (_, String groupMatch) = ("Hello you" =~ r/Hello (\w+)/).groups()

Wait for it, there's an even better way to do that

def (String groupMatch) = ("Hello you" =~ r/Hello (\w+)/).definedGroups()

The defined groups only return the groups you defined in the regex, and therefore skip the group corresponding to the whole match.

Truthy pattern declaration

Pattern pattern = r/Hello (\w+)/;

if ("yellow me" =~ pattern) {
  println("It matched????")
}

if (Matcher m = "Hello me" =~ pattern) {
  println("It matched" + m.group(1))
}

Tools

This section describes the different tools to take your Marcel experience to the next level.

Marcl

MarCL (MARcel Command Line tool) is useful to compile/execute marcel source files/scripts.

Let's explore all the commands it provides

Execute

This is the default command, meaning that if you don't specify a command, it will use this one.

Usage: marcl execute [OPTIONS] FILE [SCRIPT_ARGUMENTS]...

  Execute a marcel script

Options:
  -c, --keep-class         keep compiled class files after execution
  -j, --keep-jar           keep compiled jar file after execution
  -p, --print-stack-trace  print stack trace on compilation error
  -h, --help               Show this message and exit

Examples

marcl execute script.mcl
marcl -c script.mcl
marcl execute -cj script.mcl myScriptArg1 myScriptArg2

Compile

Usage: marcl compile [OPTIONS] FILE

  Compiles a Marcel class to a .class file and/or .jar file

Options:
  -c, --class              Compile to class
  -j, --jar                Compile to jar
  -p, --print-stack-trace  print stack trace on compilation error
  -h, --help               Show this message and exit

Examples

marcl compile script.mcl
marcl compile -cj script.mcl

Marshell

Marshell is a shell that can be used to run marcel instructions, on the fly. It is a Read Eval Print Loop tool.

It is the equivalent of groovysh for Marcel.

This shell supports syntax highlighting and also highlights defined functions/variables.

Global variables

In marshell, you can't have class fields, but you can use global variables. To declare a global variable, just assign to a variable a value, without specifying its type

a = 1 // this will create a global variable a

We didn't declare the variable a. If we were in a regular class/source file this wouldn't compile, but in Marshell this is possible.

The type of the global variable is determined by the value provided, and it can't change type. Meaning that after having defining one, you can't assign to it a value that is of an incompatible type of the one you used when you first assigned it a value.

a = 1

doSomething(a)

a = "2" // Semantic Error: Expected expression of type int but gave String

To specify explicitly the type of the global variable, use the as keyword.

a = [1, 2] as set<int>

How global variables works

Global variables are variables that are stored in the script's Binding. The means you could also retrieve them/set them using methods like Script.getVariable(name)/Script.setVariable(name, value)

Define functions

Define functions as you would in a Marcel script

Define classes

Define classes as you would in a Marcel script. All defined classes are top-level classes (they are not inner class as they would be in a Marcel script).

Run commands

Marshell has some specific commands make your experience even better.

Use the :help command to see all the commands (marshell-specific instructions) you can run

Import dependencies

You can import dependencies on the fly with the :pull command

marshell:000> :pull com.google.code.gson:gson:2.10.1
marshell:000> :import com.google.gson.Gson
marshell:000> gson = new Gson()

Initialisation script

If you want to always load some data everytime you run marshell, you can create a script in $MARCEL_HOME/marshell/init.mcl.

In this script you can't use commands. If you want to import a dependency/dumbbell, just do it like you would in a normal marcel script (import ... or dumbbell '...')

Marshell initialization scripts

Here are some useful initialization scripts that you can use for your shells. These scripts provide useful methods and utilities to enhance your marshell experience, given a context.

File explorer initializer

With the below script, transform Marshell into a file explorer, with shell-like methods such as cd, ls and a variable pwd to get the current directory.

pwd = System.getProperty("user.home") ? new File(System.getProperty("user.home"))
    // for Marshell android compatibility
    : getVariable<File>('ROOT_DIR')
if (pwd == null) {
  println("WARNING: Couldn't initialise properly pwd")
}
_hint = pwd


fun File cd(String path) {
  File f = pwd.child(path)
  if (!f.exists()) throw new IllegalArgumentException("Directory $f doesn't exists")
  if (!f.isDirectory()) throw new IllegalArgumentException("File $f isn't a directory")
  pwd = f
  _hint = pwd
  return f
}


fun File file(String path) -> pwd.child(path)

fun void ls() {
  File[] files = pwd.listFiles()
  if (files != null) {
    for (File f in files) println("- $f")
  }
}

Marcel for Android

Marcel for Android is the Marshell app made for Android, but with additional features. You can download it here.

Note that it was designed for dark theme so this app is much more UI friendly in dark mode. An update will come soon(-ish) to better handle light mode.

With this app you can

  • Run Marcel shells, as you would using Marshell
  • edit Marcel source codes
  • Schedule Shell Works, a concept allowing you to run Marcel scripts in the background, with the possibility to schedules the works and make them periodic.

Initialization scripts

You can also use Marshell's initialization scripts to enhance your experience.

Dumbbell - Marcel's dependency manager

Dumbbell is a dependency manager for Marcel. It allows you to easily import dependencies in your scripts, without having to create a Maven/Gradle project.

It was inspired strongly from Groovy's grapes


Import dependencies in a script

To import a dependency, use the dumbbell keyword.

dumbbell 'com.google.code.gson:gson:2.8.6'
import com.google.gson.Gson

Gson gson = new Gson()

println(gson.toJson(['a': 'b']))

Dependencies are pulled from Maven central. The list of repository to pull from will be configurable (someday).

Note that this feature only works when running scripts with MarCL.

Import dependencies in Marshell

Dumbbell is also used in Marshell. Use the :pull command to pull dependencies dynamically.

Maven Plugin for Marcel

Here is the official plugin to compile marcel source files under a Maven project. The Marcel source files should be placed under the src/main/marcel directory.

Link here

IntelIJ Plugin for Marcel

Here is the IntelIJ plugin to support Marcel on your IDE. This plugin has very limited features (it only does syntax highlighting...)

Link here

Marcel In Action

Here are example of projects using marcel

Fibonacci suite

Here is an implementation of the Fibonacci suite in Marcel.

println(fibonacci(10))

fun int fibonacci(int n) {
  return switch (n) {
    0, 1 -> n
    else -> fibonacci(n - 1) + fibonacci(n - 2)
  }
}

But this implementation takes a lot of time when using large n values. A way to solve this problem would be to cache fibonnaci's results, which can be done using the @cached annotation

println(fibonacci(10))

@cached
fun int fibonacci(int n) -> switch (n) {
  0, 1 -> n
  else -> fibonacci(n - 1) + fibonacci(n - 2)
}

Advent of code

Checkout Tambapps's Advent Of Code