The prog macro extends Jamal to execute a simple BASIC like programming language.
Using this integration module, you can mix Jamal macro text with simple, imperative code snippets. To use this module, the dependency:
<dependency>
<groupId>com.javax0.jamal</groupId>
<artifactId>jamal-prog</artifactId>
<version>2.8.3-SNAPSHOT</version>
</dependency>
is needed.
The dependency is added to the different Jamal integrations.
This module is available automatically when you use Jamal in Maven extension, JavaDoc, through JBang, or in the IntelliJ Asciidoctor plugin.
In other environments you may need to add the dependency manually or load the module using the macro maven:load
.
The language is a subset of the BASIC language, and it is straightforward by design. It is far simpler than ScriptBasic, Ruby or Groovy, which are also integrated into Jamal by other modules. On the other hand, it is tightly integrated into Jamal. The other programming modules allow nesting programming code into Jamal text and then use macros to manage the input and the output of the snippets.
This module also allows nesting code into Jamal and the other way around, but you do not need extra macros to transfer data between the macro and the program code. Program code can use macros as variables, function-, and method-calls. The output of the program is directly the output of the code macro. Thus, there is no need for extra macros to manage the input and the output of the code.
The language is line oriented BASIC Like language. There are only a few commands:
-
variable assignment
-
if-then-else
-
for loops
-
while loops
-
output, denoted by
<<
-
comment following
rem
or and'
character till the end of the line
Expressions can contain
-
the basic operations:
+
,-
,*
,/
,%
,.
, -
comparisons
==
,!=
,<
,>
,<
=, `>=
-
logical operations
and
,or
,not
-
and parenthesis.
In addition to these, there is a special operator !
used to start evaluation of Jamal macros in strings.
There are two macros in the package:
-
program
is the macro that executes a program -
expression
is the macro that evaluates an expression -
decimal
to support decimal number calculations, which is not part of the language, as it is necessary only rarely.
The macro program
takes the input of the macro and interprets it as a program.
The result of the macro is the concatenated strings of the output <<
commands.
For example:
{@program
<<"this is the output "
<<"of the program "
' here we start a loop
for i=1 to 2
rem this is also a comment
<<"and it is,.. "
next ' I can write comments at the end of the line
<<"concatenated "
<<"together "
}
will result in the output:
this is the output of the program and it is,.. and it is,.. concatenated together
The language is elementary, documented below in the section about expressions and commands.
The command program
has the option stepLimit
.
This parameter has to be a number.
It limits the number of steps that the program can execute.
When the limit is reached, the program will throw an exception.
The default value of the limit is 100000 (hundred-thousand).
This limit is to prevent infinite loops.
{@try! {@program stepLimit=18
<<"this is the output "
<<"of the program "
for i=1 to 2
<<"and it is,.. "
next
<<"concatenated "
<<"together "
}}
will result in the output:
Step limit reached
How the steps are counted
Each code "block" is a step, each individual command is a step, and each operation in an expression is a step. In the example above, the steps are
-
program start counts as one step
-
the first
<<
-
accessing the constant string value
-
the second
<<
-
accessing the constant string value
-
starting the for loop
-
accessing constant
1
for the start value -
accessing constant
2
for the end value -
accessing constant
1
for the step value, which is implicit, still accessing it is a step -
starting the code block of the for the first loop execution
-
the first execution
<<
in the for loop -
the access to the constant string value in the first loop execution
-
starting the code block of the for the second loop execution
-
the second execution
<<
in the for loop -
the access to the constant string value in the second loop execution
-
accessing the constant string value
-
the
"concatenated "
<<
fater the loop has finished -
accessing the constant string value
-
the
"together "
<<
-
accessing the constant string value
The command next
does not calculate.
That command is just a marker for the end of the loop, and it is used by the syntax analyser.
If you change the stepLimit
to 19 then the program will succeed.
The macro also supports the prog
, do
and run
aliases.
The macro expression
takes the input of the macro and interprets it as an expression.
The result of the macro is the value of the expression.
{@expression << 13 + 17}
will result in
30
Note
|
The macro expression is essentially the same as the prog macro.
The only difference is that the parameter options have to be enclosed between ( and ) characters and the first, and presumably only line of code can start following the closing ) character.
Its intended use is to evaluate simple expressions.
|
Caution
|
Do not forget the << at the start of the expression.
|
Expressions are composed of numeric constants, strings, variables, and operators.
The operators are the basic arithmetic operators, the comparison operators, the logical operators and the !
operator (which is macro evaluation operator, not to be confused with the operator not
).
The formal syntax of the expressions is:
expression ::= expression1 `or` expression | expression1
expression1 ::= expression2 `and` expression | expression2
expression2 ::= expression2 `==` expression | expression2 `!=` expression | expression2 `<` expression | expression2 `>` expression | expression2 `<=` expression | expression2 `>=` expression | 'not' expression1 | expression2
expression3 ::= expression3 `+` expression | expression3 `-` expression | expression3
expression4 ::= expression4 `*` expression | expression4 `/` expression | expression4 `%` expression | expresson4 `.` function_call | expression4
expression5 ::= `!` expression | `(` expression `)` | number | string | variable | function_call | `+` expression4 | '+' expression4 | '-' expression4
function_call ::= identifier `(` expression_list `)`
The different operations are executed using BigInteger numbers if the operands are both numeric.
In other cases, string operations are used.
In this second case -
, *
, /
and %
are not defined and will cause syntax error.
+
is defined as string concatenation.
When used as unary +
it is a no-op resulting the same string as the operand.
The operator !
is used to evaluate a Jamal macros.
It is applied to the expression that follows it, and it evaluates it as string.
This evaluation is done in the same scope as the surrounding environment.
If you define
here a macro then it will be available in the surrounding environment.
There is no need to export
the macro.
The same is true for the variables. If you assign a value to a variable here, then it will be available in the surrounding environment as a macro. The other way around, if you assign a value to a macro in the surrounding environment, then it will be available in the program as a variable. Such macros must not have parameters.
The following code uses two macros.
One with parameter and it is used in a try
block to catch the exception.
The other macro, b
has no parameter, and it can be used in the program.
The macro b
is used as a variable, and it is outputted from the program.
It is also modified.
The character 1
is appended to the string.
The variable evil
is not defined in the program, but it is used outside.
{@define a(x)=this is x}
{@define b =this is b}
{@try! {@program
<<a}} <-- this is an error message
{@program
<<b + !"\n{a A}" 'macro 'a' has an agument 'A', and it is used
b = b + 1 'b is not a number, therefore, 1 is appended to the string
evil = 666
}
{b} <-- already outside the program code
{evil} <-- it was defined in the program code
Macro 'a' needs 1 arguments and got 0 <-- this is an error message
this is b
this is A
this is b1 <-- already outside the program code
666 <-- it was defined in the program code
The interpreted language has only a very few commands. These are
-
variable assignment
-
if-then-else
-
for loops
-
while loops
-
output
In the following chapters, we will discuss these commands.
The variable assignment has the form:
variable = expression
The name of the variable can be any string, which is a valid Jamal user defined macro name. The value of the expression will be treated as a string and will be stored in the macro registry.
{@program
:z = 13
b = z + 17
}{b}
will result
30
The variable :z
is registered in the global macro scope.
It can later also be referenced as z
unless there is a variable z
in a lower scope.
If-then-else has the form:
if expression then block elseif expression then block else block endif
The else
and elseif
parts are optional.
The interpretation of the commands is the conventional.
{@program
if "true" then
<<"if true"
elseif true then
<<"elseif true"
else
<<"else"
endif
}
will result
if true
You can write endif
and elseif
as end if
and else if
.
For loops execute the lines between the for
and next
commands.
{@program
for i=0 to 9 step 1
<<i
next
}
will output
0123456789
The step
part is optional.
The values are evaluated when the loop is started.
The step
value can be negative.
Note that the for loop uses the same operation as +
.
It means, if some values are strings then the loop will concatenate the step value.
Unfortunately, in this case the loop cannot terminate.
{@program
i = 0
while i < 10
<<i
i = i + 1
wend
}
will result
0123456789
As we discussed before, you can use parameterless macros by the name as a variables. You can also use a macro as a method or function call even when it has parameters.
The syntax of a function call is
macroName( expression, expression, ... )
In this case, the expressions will be passed to the macro as argument or arguments.
A method call is similar, but it has a receiver "object".
receiver.macroName( expression, expression, ... )
The receiver is an expression. The method call is a syntactic sugar only and it is equivalent to
macroName( expression, expression, ... , receiver)
Caution
|
The receiver is the last argument, not the first. It is not the same as usual. It makes sense when built-in macros are called this way chained. The macros work one after the other on each others output, and the options can be specified as parameter strings. |
The macro can be a user defined macro or a built-in macro. First, the user defined macro registry is consulted, and the built-in macros are only considered when the user defined macro is not found.
The argument mapping is simple in the case of user defined macros.
User defined macros have several arguments, and the arguments of the function call are mapped to the arguments of the macro in the order of the arguments.
You can also use argument less macros, simply not writing anything between the ()
characters.
This is almost the same as using the macro as a variable.
The only difference is that putting ()
characters after the macro name will make it a function call to a built-in macro in the case the user-defined macro does not exist.
Without the ()
an undefined macro will create an error even if a built-in macro exists with the same name.
In the case of built-in macros, the mapping is not so trivial. Built-in macros have one single string parameters, which is not split up to separate arguments by default. The macro implementation may split up the string to separate arguments, but it is not the default behavior. Many macro implementations apply specific and unique syntax.
The mapping joins the arguments of the call to a single string.
It takes the arguments but the last one and joins them separating with a space each.
After that this string is surrounded with ()
characters or whatever the macro can use to enclose options.
Finally, the last argument is appended to the string.
This way the arguments at the start are options for the macro call. Now this may also explain why the receiver is the last argument when the syntax is a method call.
Note
|
You cannot call a macro that has a name, which is a keyword in the language.
The keywords are if , else , elseif , then , endif , while , wend , for , next , do , until , and , or , not , to , step , end .
|
There is one major difference between evaluating a user-defined macro in the macro environment and calling it as a function.
User-defined macros are processed for further macros after they produce their result unless they are a verbatim type.
Calling a macro as a function does not process the result further.
If you need the result processed, you should use the !
operator.
Variable assignment automatically defines argument less macros.
You can define macros with arguments using the define
macro evaluating strings or calling the define macro as a function.
Let’s have a look at the following example.
{@prog
<< "here we go\n"
_ = define( "a(x)=this is x\n" )
<< a( "A" )
}
{_} is empty
{a /'is just a'}
will result in
here we go
this is A
is empty
this is 'is just a'
It also shows that the macros defined inside the program code part can also be used inside and outside in the same context.
The following example will show macros chained as method calls. It also gives a good example explaining why the receiver is the last argument.
{@program
<< "this is a string"
<<"\n"
<< "this is a string".string:substring("begin=1","end=5")
<<"\n"
<< "this is a string".string:substring("begin=1","end=5").case:upper()
<<"\n"
<< "this is a string".string:substring("begin=1","end=5").case:upper().string:chop("post=S")
}
will result in
this is a string
his
HIS
HI
The implemented BASIC language itself supports only integers. The reason for that was to keep the language simple. In most of the cases, integer arithmetic should be enough and there is less room for errors.
Note
|
Versions before 2.8.0 used only integer arithmetic.
To amend this the macro decimal was introduced.
The use of this macro is cumbersome, and it is only kept in the package as a legacy support.
|
If you want to use an expression or program to use floating point arithmentic you have to use the parop float
.
{@program float
<< 13.2/2.5
}
will result in
5.28
The calculations are executed using the BigDecimal
class.
The other related parameter options you can use are:
-
float
to use floating point arithmetic -
scale
to set the scale of theBigDecimal
numbers -
rounding specification, that can be one of
-
UP
round up in absolute value, e.g. -1.1 → -2 -
DOWN
round down in absolute value, e.g. -1.1 → -1 -
CEILING
round up signed, e.g. -1.1 → -1 -
FLOOR
round down signed, e.g. -1.1 → -2 -
HALF_UP
round to the nearest or up. This is the default rounding if nothing is specified. -
HALF_DOWN
round to the nearest or down. -
HALF_EVEN
round to the nearest or to the even number. -
UNNECESSARY
throws an exception if rounding is necessary.
-
These are the actual names of the Java enumeration java.math.RoundingMode
and their meaning is as it is documented in the JavaDoc of that class.
Note
|
The features decsribed in this section are only for backward compatibility and their use is not recommended. |
To support floating point calculation, the package implements the macro decimal
.
This macro creates a named BigDecimal number.
For example
{@do
x = decimal("pi=3.1415926")
}
will return
The macro decimal
in the example above defines five global macros.
-
pi
to return the value of the decimal number named pi -
pi:add
to add one or more decimal numbers to the number named pi -
pi:sub
to subtract one or more decimal numbers from the number named pi -
pi:mul
to multiply the number named pi with one or more decimal numbers -
pi:div
to divide the number named pi with one or more decimal numbers
The name of the macros is always the name of the decimal number as specified in the argument to the macro decimal
followed by a colon and the operation.
The next example performs operations using these possibilities:
{@do
x = decimal("scale=4","pi=3.1415926")
<< pi
<< "\n"
<< pi:add("1.0")
<< "\n"
<< pi:sub("0.141")
<< "\n"
<< pi:mul(2)
<< "\n"
<< pi:div(3)
}
will result in
3.1416
4.1416
4.0006
8.0012
2.667066666666666666666666666666667
The parop scale
sets the BigDecimal
scale of the number.
This value is important for the div
operation.
Also, the rounding is set to HALF_UP
.
You can call these macros as real macros without using the BASIC interpreter. The following example shows the traditional use of these macros:
{@decimal(scale=4)pi=3.1415926}
{pi}
{pi:add/1.0}
{pi:sub/0.141}
{pi:mul/2}
{pi:div/3}
It will result in the same output.
3.1416
4.1416
4.0006
8.0012
2.667066666666666666666666666666667
Originally, these macros are meant to be used as functions in the BASIC language.