Skip to content

Latest commit

 

History

History
733 lines (553 loc) · 19.2 KB

README.adoc

File metadata and controls

733 lines (553 loc) · 19.2 KB

Jamal Prog Macro Module

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.

Macros in the Package

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.

Program

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:

Jamal source
{@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:

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.

Jamal source
{@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:

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

  1. program start counts as one step

  2. the first <<

  3. accessing the constant string value

  4. the second <<

  5. accessing the constant string value

  6. starting the for loop

  7. accessing constant 1 for the start value

  8. accessing constant 2 for the end value

  9. accessing constant 1 for the step value, which is implicit, still accessing it is a step

  10. starting the code block of the for the first loop execution

  11. the first execution << in the for loop

  12. the access to the constant string value in the first loop execution

  13. starting the code block of the for the second loop execution

  14. the second execution << in the for loop

  15. the access to the constant string value in the second loop execution

  16. accessing the constant string value

  17. the "concatenated " << fater the loop has finished

  18. accessing the constant string value

  19. the "together " <<

  20. 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.

Expression

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.

Jamal source
{@expression << 13 + 17}

will result in

output
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.

Expression Syntax

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.

Jamal source
{@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
output
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

Commands

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.

variable assignment

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.

Jamal source
{@program
     :z = 13
     b = z + 17
}{b}

will result

output
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

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.

Jamal source
{@program
if "true" then
    <<"if true"
elseif true then
    <<"elseif true"
else
    <<"else"
endif
}

will result

output
if true

You can write endif and elseif as end if and else if.

for loops

For loops execute the lines between the for and next commands.

Jamal source
{@program
for i=0 to 9 step 1
    <<i
next
}

will output

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.

while loops

Jamal source
{@program
i = 0
while i < 10
    <<i
    i = i + 1
wend
}

will result

output
0123456789

output

The output command, as you could see examples in the previous samples, is the << command. The expression after the << is evaluated and the result is appended to the result of the program macro.

Jamal source
{@program
   a = "{@define z=55}"
   a = !a
   <<a
   <<z

} {z}
output
55 55

Function and Method Calls

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

Jamal source
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".

Jamal source
receiver.macroName( expression, expression, ... )

The receiver is an expression. The method call is a syntactic sugar only and it is equivalent to

Jamal source
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.

Jamal source
{@prog
<< "here we go\n"
_ = define( "a(x)=this is x\n" )
<< a( "A" )
}
{_} is empty
{a /'is just a'}

will result in

output
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.

Jamal source
{@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

output
this is a string
his
HIS
HI

Working with Decimals

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.

Jamal source
{@program float
 << 13.2/2.5
}

will result in

output
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 the BigDecimal 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.

Legacy support to handle Decimals

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

Jamal source
{@do
  x = decimal("pi=3.1415926")
}

will return

output

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:

Jamal source
{@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

output
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:

Jamal source
{@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.

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.