DevelopingStatements - gama-platform/gama GitHub Wiki
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).
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.
| 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 theoutputsection of experiments (display, layers...); -
ACTION,PARAMETER,BATCH_METHOD,EXPERIMENT,SPECIES,MODEL...: other kinds of GAML elements.
@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.
@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. |
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.
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.
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 withgetFacet("name"), which returns anIExpressionornull. Literal facets (identifiers) can be read withgetLiteral("name"). -
privateExecuteIn(IScope scope)is called each time the statement is executed. It is called byexecuteOn(IScope)(inherited fromAbstractStatement), which also records the current statement in the scope and adds the context of anyGamaRuntimeExceptionthrown. The value returned is the result of the statement (used for instance byreturn), usuallynull.
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 theCastmethods (Cast.asString,Cast.asInt,Cast.asFloat...) or withgetFacetValue(scope, "name", defaultValue); - errors are reported by throwing
GamaRuntimeException.error("...", scope); warnings byGAMA.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.
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.
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.
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.