Skip to content

How to Add a Primitive

Aaron Brandes edited this page Jan 23, 2024 · 3 revisions

The first law of adding primitives is: DON'T. The language is much too big already.

In the name of all that is holy, write an extension instead!

Basics

Adding a new command or reporter to NetLogo is fairly straightforward. The system has been designed for modular addition of primitives. Just drop your prim into the headless/src/main/org/nlogo/prim/etc directory and add the prim to netlogo-gui/resources/system/tokens.txt, shared/resources/main/system/tokens-core.txt, parser-jvm/src/test/parse/TestAllSyntaxes.scala and netlogo-gui/src/test/compile/TestAllSyntaxes.scala. Don't forget to add it to the User Manual, too! Dictionary entries belong in autogen/docs/dictionary.html.mustache. And add tests to test/commands or test/reporters.

Creating the file

We have adopted certain naming conventions for primitive file names.

  • all primitive file names begin with an underscore character "_"
  • primitive file names should be only letters and numbers, all lowercase.

Some examples:

  • _createturtles.java defines create-turtles aka crt
  • _greaterthan.java defines the > operator
  • _repeat.java defines repeat
  • _dump.java defines __dump (double underscores are used for internal-use-only and/or undocumented primitives)

The easiest way to make a new primitive is to use an old primitive as a template.

Coding notes

You'll want to at least skim Engine architecture.

Even if you write the prim in Scala, make sure the perform/report functions (with a number) use strict-Java style (that is, no map, foreach and so on). match, however, can be used.

NOTE: sorry, this section is out of date User:SethTisue 15:47, 4 Apr 2008 (CDT)

a prim must be either a command or a reporter -- specify which by subclassing either Command or Reporter.

the string in the constructor indicates which agent types can use the primitive -- "OTP" for any, "TP" for turtle or patch, "T" for just turtle, etc.

implementing the getSyntax() method lets you specify what argument types your prim takes, and if it's a reporter, what type it returns

Commands

NOTE: sorry, this section is out of date User:SethTisue 15:47, 4 Apr 2008 (CDT)

the "false" or "true" in a Command's constructor says whether the command switches, which is to say, whether control of execution will switch to the next agent after the command runs. (The Reporter constructor doesn't take this flag as Reporters never switch.)

commands that take command blocks use a special form of the constructor that uses a third argument to specify what kind of agent will be running the commands in the block (e.g. hatch and sprout specify it will be turtles)

To define what a Command does, define the perform() method, which has a void return type. The perform() method takes a Context argument that gives it access to the executing agent, the stack, etc. It also has access to the Workspace and World objects through instance variables in Instruction (which is the common superclass of Command and Reporter).

for most Commands, the perform() method ends by incrementing the instruction pointer; some control-flow commands like _if do special things instead with the instruction pointer

Reporters

NOTE: sorry, this section is out of date User:SethTisue 15:47, 4 Apr 2008 (CDT)

to define what a Reporter does, define the report() method, which returns an Object. The report() method takes a Context argument that gives it access to the executing agent, the stack, etc. It also has access to the Workspace and World objects through instance variables in Instruction (which is the common superclass of Command and Reporter).

Reporters should implement the Pure interface if they are either:

  • Constant (e.g. _constinteger, _conststring)
  • Constant-preserving (i.e. the result is constant when all of their args are constant.)

The main point here is that this distinction allows us to compute some values at compile-time, rather than run-time.
The result of "Pure" reporters must not depend on the state of the "context", "workspace", or "world".

Exceptions

NetLogo runtime exceptions are represented by the LogoException class, and EngineException which is a subclass of LogoException. LogoException is a checked exception.

If the user's Logo code tries to do something illegal, it should result in a LogoException (preferably an EngineException, see below). Only throw unchecked exceptions like RuntimeException, IllegalStateException (which is a subclass of RuntimeException) , etc. in cases where there is a bug in the Java code. There shouldn't be any Logo code the user can possibly write that will produce a RuntimeException.

in the org.nlogo.prim package, if you want to produce a runtime error with a custom error message, use: throw new EngineException( this , "message" ) . (EngineException is a subclass of LogoException.) It is better to throw an EngineException than a generic LogoException since the EngineException contains the instruction where it happened, so the error reporting can pinpoint the error location more specifically.

if your perform/report method throws a Java exception (other than a LogoException), it will be caught and handled by NetLogo's runtime error system -- but you should try to throw a EngineException (or, if you must, a generic LogoException, but try to use EngineException, since it has a place to store the Instruction where the error happened) instead, since other Java exceptions should never be seen by the user. A subclass of EngineException called ArgumentTypeException is also available, for when you need to manually check the type of your inputs.

In the org.nlogo.agent.package, you don't have access to EngineException. A number of methods in org.nlogo.agent use AgentException instead, a checked exception which is not a LogoException. How it works is, the methods in org.nlogo.agent throw AgentException; code in org.nlogo.prim that invokes those methods should catch AgentException, and, if appropriate, throw an EngineException instead.

Dead turtles

The engine already ensures that a dead turtle never executes code, so you don't need to worry that your new primitive might be run by a dead turtle.

The engine also ensures that when you iterate over an agentset of turtles, no dead turtles are returned.

You still sometimes need to add checks for whether a turtle is dead, and that's for primitives that can take a single turtle as input. Examples include _turtlevariableof, _follow, _ask, and so on. Whenever you use reportObject(), reportAgent(), or reportTurtle(), consider whether you need to add an explicit check to see if the result is a dead turtle.

Clone this wiki locally