DevelopingStatements - gama-platform/gama GitHub Wiki

Developing Statements

A statement is a GAML keyword that performs an action when it is executed, such as write, ask, create or draw. It is defined by a Java class extending gama.api.gaml.statements.AbstractStatement (or AbstractStatementSequence when it has a body), annotated with @symbol, @inside, @facets and @doc.

The class must be in a GAMA plugin (see Developing Plugins). There is no wizard for statements: create the class by hand in the src/ folder. To study the implementation of an existing statement, use GAMA > Search GAML keyword... (e.g. statement:warn).

Example: the warn statement

gama.core/src/gama/gaml/statements/WarnStatement.java defines warn "message";, which displays a warning in the error view:

import gama.annotations.doc;
import gama.annotations.example;
import gama.annotations.facet;
import gama.annotations.facets;
import gama.annotations.inside;
import gama.annotations.symbol;
import gama.annotations.usage;
import gama.annotations.constants.IKeyword;
import gama.annotations.support.IConcept;
import gama.annotations.support.ISymbolKind;
import gama.api.GAMA;
import gama.api.compilation.descriptions.IDescription;
import gama.api.exceptions.GamaRuntimeException;
import gama.api.gaml.expressions.IExpression;
import gama.api.gaml.statements.AbstractStatement;
import gama.api.gaml.types.Cast;
import gama.api.gaml.types.IType;
import gama.api.kernel.agent.IAgent;
import gama.api.runtime.scope.IScope;

@symbol (
        name = IKeyword.WARN,
        kind = ISymbolKind.SINGLE_STATEMENT,
        with_sequence = false,
        concept = { IConcept.SYSTEM })
@inside (
        kinds = { ISymbolKind.BEHAVIOR, ISymbolKind.SEQUENCE_STATEMENT, ISymbolKind.LAYER })
@facets (
        value = { @facet (
                name = IKeyword.MESSAGE,
                type = IType.STRING,
                optional = false,
                doc = @doc ("the message to display as a warning.")) },
        omissible = IKeyword.MESSAGE)
@doc (
        value = "The statement makes the agent output an arbitrary message in the error view as a warning.",
        usages = { @usage (
                value = "Emmitting a warning",
                examples = { @example ("warn \"This is a warning from \" + self;") }) })
public class WarnStatement extends AbstractStatement {

    final IExpression message;

    public WarnStatement(final IDescription desc) {
        super(desc);
        message = getFacet(IKeyword.MESSAGE);
    }

    @Override
    public Object privateExecuteIn(final IScope scope) throws GamaRuntimeException {
        IAgent agent = scope.getAgent();
        String mes = null;
        if (agent != null && !agent.dead()) {
            mes = Cast.asString(scope, message.value(scope));
            GAMA.reportAndThrowIfNeeded(scope, GamaRuntimeException.warning(mes, scope), false);
        }
        return mes;
    }
}

IKeyword (in gama.annotations.constants) holds the keywords of the core; in a plugin, declare your own String constants for the names of your statement and facets.

Declaring the statement: @symbol

Attribute Meaning
name (mandatory) The keyword(s) of the statement (several names define synonyms).
kind (mandatory) Its kind, a value of the enum gama.annotations.support.ISymbolKind (see below).
with_sequence (mandatory) true if the statement has a body ({ ... }) containing other statements.
with_scope Whether the body defines a new scope for temporary variables (default true).
with_args true if the statement accepts arguments (like do or create ... with: [...]).
remote_context true if the body is executed in the context of other agents (like ask or create), so that myself is available.
breakable, continuable Whether break / continue can be used in its body (loops).
unique_in_context true if it can appear only once in its enclosing statement.
unique_name true if its name must be unique in its context.
concept, category Concepts (IConcept constants) and categories used for the documentation.
internal true to hide the statement from the documentation.
doc Its documentation (it can also be an @doc placed on the class, as above).

The main kinds used by statements are:

  • SINGLE_STATEMENT: a statement without body (write, warn, do...);
  • SEQUENCE_STATEMENT: a statement with a body of statements (if, loop, ask...);
  • BEHAVIOR: a behavior of a species (reflex, init, state...);
  • LAYER, OUTPUT: elements of the output section of experiments (display, layers...);
  • ACTION, PARAMETER, BATCH_METHOD, EXPERIMENT, SPECIES, MODEL...: other kinds of GAML elements.

Where it can be used: @inside

@inside restricts the places where the statement can appear: kinds lists the kinds of the enclosing elements (e.g. ISymbolKind.BEHAVIOR, ISymbolKind.SEQUENCE_STATEMENT for a statement usable in any reflex, action or block), and symbols lists explicit keywords (e.g. symbols = { IKeyword.CHART }). Using the statement elsewhere produces a compilation error.

Its facets: @facets and @facet

@facets contains one @facet per facet. Its attribute omissible gives the facet whose name can be omitted (warn "..." instead of warn message: "..."); it defaults to name.

@facet attribute Meaning
name (mandatory) Name of the facet.
type (mandatory) Accepted expression type(s), as IType constants (several allowed, e.g. { IType.INT, IType.FLOAT }), or exactly one type descriptor for a temporary declaration. IType.ID is used for identifiers, IType.NONE for any value.
declares_new_temp true to declare a temporary identifier; its type is described by type, of, and index. Defaults to false.
of, index Content and key types, for containers.
values For an IType.ID facet, the list of accepted values.
optional Whether the facet can be omitted. Defaults to false.
remote_context true if the expression is evaluated in the context of other agents.
internal true to hide the facet from the documentation.
doc Its documentation.

Declaring temporary variables

Identifier declaration is independent of variable typing. Set declares_new_temp = true and describe the variable with ordinary type IDs, using gama.api.gaml.types.IType:

// benchmark returns: declares a list<float>.
@facet(name = IKeyword.RETURNS, declares_new_temp = true,
       type = IType.LIST, of = IType.FLOAT, optional = true)

// A map<string, float> result.
@facet(name = IKeyword.RETURNS, declares_new_temp = true,
       type = IType.MAP, index = IType.STRING, of = IType.FLOAT, optional = true)

Exactly one outer type is required. of describes contents; it does not turn a scalar or unknown outer type into a list. A declaring facet is read as a literal identifier, not validated as an expression of the declared variable type.

When the type depends on the statement, import gama.annotations.support.ITypeProvider:

// create/capture/release/migrate: a list of the selected species.
@facet(name = IKeyword.RETURNS, declares_new_temp = true,
       type = IType.LIST, of = ITypeProvider.STATEMENT_TYPE, optional = true)

// let/loop/arg/user_input: use the statement's existing declaration/inference rules.
@facet(name = IKeyword.NAME, declares_new_temp = true,
       type = ITypeProvider.INFERRED_TYPE)

STATEMENT_TYPE resolves the statement's computed type, including species/target facets. INFERRED_TYPE uses its declared type, or existing value/iteration inference when no type is declared. Parameterized types remain intact, including list<list<int>> and map<string, list<float>>. Either provider may be used in type, of, or index; other negative providers are not supported here. Do not use OWNER_TYPE to mean the statement's type: it refers to an owning species/operand.

The annotation supplies static typing, not runtime assignment. Read the chosen name with getLiteral(...) and create/populate the variable in the correct runtime frame. If a sequence's result must remain available after its body, initialize the variable in the enclosing frame before super.enterScope(scope), as BenchmarkStatement.enterScope does, then assign its value during execution. Loop variables, by contrast, belong inside the loop's scope.

Migration: IType.NEW_TEMP_ID has been removed. Replace it with declares_new_temp = true and explicit type metadata, then rebuild the annotations/processor and clean/rebuild dependent bundles to regenerate GamlAdditions. Custom IArtefactFactory.createFacetArtefact callers/implementations must pass the new declaresNewTemp boolean; custom facet artefacts must expose raw content/key descriptors through getContentTypeProvider() and getKeyTypeProvider(). Existing GAML declaration syntax does not change.

Documenting: @doc

The @doc of the class describes the statement (value) and gives its usages: each @usage explains one way of using it and gives @examples of GAML code. These texts are used by the GAML editor (hover, completion) and the online documentation. Missing documentation is reported by a processor warning.

Implementing the behavior

One instance of the class is created for each occurrence of the statement in a model:

  • The constructor (public MyStatement(final IDescription desc), mandatory) is called once, when the model is compiled. It usually stores the facets as expressions with getFacet("name"), which returns an IExpression or null. Literal facets (identifiers) can be read with getLiteral("name").
  • privateExecuteIn(IScope scope) is called each time the statement is executed. It is called by executeOn(IScope) (inherited from AbstractStatement), which also records the current statement in the scope and adds the context of any GamaRuntimeException thrown. The value returned is the result of the statement (used for instance by return), usually null.

Inside privateExecuteIn:

  • scope.getAgent() returns the agent executing the statement;
  • expression.value(scope) evaluates a facet in the current context; the result can be converted with the Cast methods (Cast.asString, Cast.asInt, Cast.asFloat...) or with getFacetValue(scope, "name", defaultValue);
  • errors are reported by throwing GamaRuntimeException.error("...", scope); warnings by GAMA.reportAndThrowIfNeeded(scope, GamaRuntimeException.warning("...", scope), false);
  • getTrace(scope) returns a textual description of the statement (used by the debugger and error messages).

Do not store per-execution state in fields of the statement: the same instance is executed by all the agents.

Statements with a body

For a statement declared with with_sequence = true, extend AbstractStatementSequence, which executes its children in order (in a new scope if with_scope is true). Override setChildren(...) to keep the child statements, and privateExecuteIn(...) to decide when to run them. gama.extension.sound/src/gama/extension/sound/StartSoundStatement.java (start_sound source: "file.mp3" { ... }) runs its body after starting the sound:

@symbol (
        name = IKeyword.START_SOUND,
        kind = ISymbolKind.SEQUENCE_STATEMENT,
        concept = { IConcept.SOUND },
        with_sequence = true,
        doc = @doc ("Starts playing a music file. The supported formats are aif, au, mp3, wav. One agent"))
@inside (
        kinds = { ISymbolKind.BEHAVIOR, ISymbolKind.SEQUENCE_STATEMENT, ISymbolKind.LAYER, ISymbolKind.OUTPUT })
@facets (
        value = { @facet (name = IKeyword.SOURCE, type = IType.STRING, optional = false,
                        doc = @doc ("The path to music file. This path is relative to the path of the model.")),
                @facet (name = IKeyword.MODE, type = IType.ID, values = { IKeyword.OVERWRITE, IKeyword.IGNORE },
                        optional = true, doc = @doc ("Mode of playing the sound")),
                @facet (name = IKeyword.REPEAT, type = IType.BOOL, optional = true,
                        doc = @doc ("Should the sound be repeated or not ?")) })
@validator (StartSoundValidator.class)
@doc ("Allows to start the sound output")
public class StartSoundStatement extends AbstractStatementSequence {

    private AbstractStatementSequence sequence = null;

    @Override
    public void setChildren(final Iterable<? extends ISymbol> com) {
        sequence = new AbstractStatementSequence(description);
        sequence.setName("commands of " + getName());
        sequence.setChildren(com);
    }

    @Override
    public Object privateExecuteIn(final IScope scope) throws GamaRuntimeException {
        final IAgent currentAgent = scope.getAgent();
        // ... starts playing the file given by the source facet ...
        if (sequence != null) { scope.execute(sequence, currentAgent, null); }
        return null;
    }
}

scope.execute(statement, agent, arguments) runs a statement (or sequence) on behalf of an agent and returns an IExecutionResult.

Validating at compile time: @validator

Errors that can be detected before running the model (incompatible facets, invalid constant values...) should be reported in the GAML editor. Annotate the statement with @validator(MyValidator.class) (in gama.api.annotations), where MyValidator implements gama.api.compilation.descriptions.IDescriptionValidator and its method validate(IDescription description). The description gives access to the facets as expressions (getFacetExpr(...), hasFacet(...)) and reports problems with error(message, code) and warning(message, code) (codes are constants of gama.api.constants.IGamlIssue, e.g. IGamlIssue.GENERAL). For example, the validator of the solve statement (gama.extension.maths/src/gama/extension/maths/ode/statements/SolveStatement.java):

public static class SolveValidator implements IDescriptionValidator<IDescription> {

    @Override
    public void validate(final IDescription desc) {
        final IExpression method = desc.getFacetExpr(IKeyword.METHOD);
        if (method != null && method.isConst() && method.isContextIndependant()) {
            final String methodName = method.literalValue();
            if (Adaptive_Stepsize_Integrators.contains(methodName)) {
                if (!desc.hasFacet("min_step") || !desc.hasFacet("max_step") /* ... */) {
                    desc.error("For Adaptive Stepsize Integrators, the facets min_step, max_step, ... have to be defined.",
                            IGamlIssue.GENERAL);
                }
            } // ...
        }
    }
}

Similarly, @serializer(MySerializer.class) (in gama.api.annotations) can provide a class implementing gama.api.compilation.serialization.ISymbolSerializer to customize how the statement is written back as GAML text (see GridLayerStatement.GridLayerSerializer in gama.core). It is rarely needed.

Checking the result

After saving, the plugin is rebuilt and the statement is registered in its GamlAdditions (run GAMA > Clean GAMA plugins if it is not). Launch GAMA with GAMA > Run GAMA > Run UI, then check that the keyword is proposed by completion and documented by the hover in the GAML editor, and add a test model in the models/Tests folder of the plugin (GAMA > Create GAMA artefacts > New GAML Test Model).

See also: Developing Skills, Developing Operators, IScope, Index of annotations.

⚠️ **GitHub.com Fallback** ⚠️