Introduction

Joe is a scripting language, implemented in Java, and is meant to used to add scriptability to Java applications and libraries. This section lays out the goals (and non-goals!) of Joe, and the rationales behind them. 1

I'm implementing Joe for use in my own personal projects, and as a hobby language. Others might find it useful or interesting as well.

Goals

I wanted a language

  • For adding scriptability to Java applications and libraries,
  • With few, if any, external dependencies
  • That is Java-friendly in its syntax and semantics
  • That can work with native Java types
  • With a simple API for embedding it in an application or library
  • That provides excellent compilation and runtime error messages
  • That is naturally sandboxed at the script level
    • I.e., scripts cannot access arbitrary Java packages, the operating system, or I/O, except insofar as the client adds bindings for that purpose.
  • That provides a simple, easy to use API for writing bindings to native code

Joe is growing into that language.

Non-Goals

High Performance: I don't need it; and I'm following the original Tcl/Tk dictum: write the fast code in the host language and glue it together with the scripting language.

Application development: See above. Joe is meant to be a partner with Java, not a replacement.

General scripting: Use Python. Or, better still, Tcl/Tk.

Pure functional programming: I'm happy to incorporate functional techniques in my code; but I've spent over 40 years learning how to work with mutable state without shooting myself in the foot.

Automatic binding to Java classes: I've written bindings by hand. I've used generators like Swig. Artisanal hand-crafted bindings are much more pleasing, at least to me. I want to provide tools for that purpose, not settle for something quick and ugly.

Why Java?

I've been using Java professionally, full-time, for the last nine years. I'm comfortable with it, I'm getting good results from it, and I like the way the language is evolving. I've done lots of both non-GUI and GUI programming in Java (using the JavaFX toolkit), and it's my currently language of choice for personal projects as well.

It's robust. It's stable (at least since the Java 8/Java 9 fiasco). It's popular.

Why Not Javascript/LuaJ/JTcl/Groovy...

I've written lots of bindings for scripting languages, going back over a quarter of a century, mostly for Tcl (in C/C++) and JTcl (in Java); but I've also worked with the Nashorn version of Javascript, and the LuaJ implementation of Lua. I've experimented with Groovy.

I experimented with Groovy at one point, and bounced. I don't remember all the details, but I prefer a language in which the syntax isn't quite so... flexible.

I like Tcl very much; but Tcl is not a great match for scripting Java APIs. Done a lot of it; it's a great command/console language; but it can be a lot of work. Further, the JTcl implementation is both (A) dead, and (B) includes a host of code I have no need of; and (C) I really don't want to take over maintenance of it.

I used Nashorn for a period of time, back with Java 8; I didn't care for the API I had to use to talk to it, particular, and I had to do a lot of work to provide useful error messages. And then the powers that be deprecated it, and announced that the future alternative to was to switch to the Graal VM and run my embedded Javascript in Node. Um, no. The tail on that dog is much too big.

I've used LuaJ quite a bit. It has some nice features, but the syntax and some of the language choices are weird from a Java point of view; the error messages are frequently unhelpful; the standard library is incomplete; and I kinda hate Lua tables with a passion. (I've been acquainted with "associative arrays" since I learned Awk in the late 1980's. But Lua tables are just weird, and the standard table API has some peculiar holes.)

And then I ran into Robert Nystrom's Lox (see below), and realized that I could build on his example and produce a language that meets my needs and aesthetics.

Why No External Dependencies?

I know the trend these days is to pick a language with a package manager and then construct your app from dozens, if not hundreds, of external packages. And sure, you can do that. Me, I'll be over here not worrying about supply chain issues.

Why Sandboxed?

Because I don't want to worry about security issues; and more particularly, I don't want to give clients an easy way to shoot the host application in the head. Joe for solving domain-specific problems; it shouldn't have any more access than the domain requires.

Why not automatic access to Java classes and methods?

Even given that, why not use Java reflection so that a scripting API can automatically bind to specific, required, Java classes and methods?

LuaJ and Nashorn do this; and in each case I found that I didn't like the error messages I got if I passed an incorrect value to a native method. I ended up having to write custom Java classes that wrapped the native classes in order to provide the API I wanted with the error handling I wanted.

I want an embedding API that helps me to do a good job, not one I have to work around.

Joe, Lox and JLox

Lox is a little language described in Robert Nystrom's excellent book, Crafting Interpreters; JLox is a tree-walker implementation of the language implemented in Java.

The Joe language and implementation derive from Lox in general and JLox in particular and retains Lox semantics at its core. I've subsequently changed and added syntax, refactored the implementation, etc., etc., as my needs (and whimsy) takes me. But the only reason any of that is possible, is because Mr. Nystrom designed and built a very nice little language and wrote what is possibly the best programming text I've ever read to describe how the trick is done.2

1

Point is, I've got reasons; here they are; I'm not inclined to argue about them.

2

Should Mr. Nystrom ever find himself in my vicinity, I'll gladly buy him lunch.

Change Log

Changes in 0.7.0 (in development)

  • Internals
    • Replaced the Bert byte-code engine and its single-pass compiler with the Clark byte-code engine, which shares a parser and scanner with the Walker engine. This will make Joe easier to evolve.
      • Clark is now the default engine.
      • Bert has been removed.
  • Language
    • Added Nero, an implementation of the Datalog query language.
      • Nero can be used as a standalone language via the joe nero tool, or embedded in scripts to perform queries or translations of scripted input data.
    • Merged let's capability into var, and removed let.
      • var can now do destructuring binds while still handling simple variable declarations efficiently.
    • foreach can now do a destructuring bind on each item in the collection.
    • The new ~ operator is a comparison operator that checks whether a Joe value matches a Joe pattern, binding any variables in the local scope. It replaces the existing if let statement, which has been removed.
    • Replaced the "instance" pattern syntax with the friendly "named-field" pattern syntax.
    • "record" pattern syntax is now called "ordered-field" pattern syntax, as we now have non-record-types with ordered fields.
    • Both "named-field" and "ordered-field" patterns can match the Fact objects produced by Nero rule sets.
  • Extensibility
    • Simplified implementation of proxy types for native record types.
      • Any ProxyType can now easily define read-only fields for its proxied values.
  • Tools
    • Revised joe dump to output one or all of the following:
      • The script's byte-code (the default)
      • The script's Abstract Syntax Tree (AST)
      • The script's legacy Bert byte-code
    • Revised the AST dump format for readability and easy maintenance.

Changes in 0.6.0 (28 March 2025)

  • Language
    • Lists may now be created with list literal syntax, e.g., [1, 2, 3].
      • List items may be any arbitrary expression.
    • Maps may now be created with map literal syntax, e.g., {#a: 1, #b: 2, #c: 3}.
      • Map keys and values may be any arbitrary expressions.
    • Both lists and maps may now be accessed using array notation, e.g., var x = myList[i].
      • Array notation is sugar for the List get and set methods, and for the Map get and put methods.
    • Added the let statement, which performs destructuring binds using pattern matching.
    • Added scripted record types.
    • Added the if let statement, which performs conditional destructuring binds.
    • Added the match statement, which matches a target value against a number of case patterns and executes the selected statement.
  • Internals/Embedding/Extending
    • Refactored the names of Joe's type-related interfaces and Java classes for clarity.
    • Added infrastructure for creating Joe bindings for Java record types See CatchResult for an example.
      • The infrastructure can also be used with non-record types that should look like records at the script level, i.e., should have an immutable ordered list of immutable fields.
      • Added JoeType::isRecordType to support this infrastructure.
  • Standard Library
    • Added the Type type as the root of the Joe type system.
    • Added the Joe type as the host of Joe's introspection (aka reflection) API. Also, moved several global functions to be static methods of Joe:
      • Joe.compare()
      • Joe.currentTimeMillis() (replacing the global millis() function)
      • Joe.stringify()
    • Added better support for opaque types.
      • Values of opaque types are now provided with an ad hoc proxy that provides a toString() method.
      • Joe.typeOf(opaqueValue).name() is the Class::getName value for the Java type.
    • Added the CatchResult type as the result of the catch() method, replacing the Tuple type.
    • Deleted the Tuple type, as it now seems ill-conceived.

Changes in 0.5.0

  • Language

    • A default toString() method is defined automatically for instances of Joe classes.
      • And can be overridden by the class declaration.
      • It is always the case that stringify(instance) == instance.toString() for instances of Joe classes.
    • Greatly improved JoeError's stack trace output. It now includes, for each stack level,
      • Complete function signatures, where available
      • Source file and line number, where available
  • Bert Byte-code Engine

    • Added BertEngine, a byte-code execution engine, following Nystrom's clox design in Java.
    • BertEngine and WalkerEngine both implement the entire Joe language and support the entire Joe standard library, as verified by the Joe test suites.
    • BertEngine is now enabled by default.
    • The WalkerEngine can enabled by the -w option on relevant joe tools.
  • Extending/Embedding API

    • Added Joe::isComplete.
    • It is now possible to create bindings for native types that can be extended by scripted classes.
  • Library

    • Experimental joe.win Package
      • Added JavaFX widgets: Menu, MenuBar, MenuItem, Separator, Tab, TabPane, ListView, SplitPane, GridPane
      • Added JavaFX enums: Orientation, Side
  • Bugs fixed

    • Test script print/println output is now hidden unless joe test is run with the --verbose option.

Changes in 0.4.0

  • Language

    • Added the @ operator.
      • In class methods, @name is identical to this.name.
  • Extending/Embedding API

    • It is now possible to define native types that more fully resemble Joe classes and instances, e.g., the new TextBuilder.
      • Instances of TextBuilder have data fields, just like Joe class instances.
      • TextBuilder can be extended by Joe classes.
    • Moved the arity checking methods (e.g., Joe.exactArity()) from Joe to Args.
  • Library

    • Added experimental joe.win package for creating JavaFX GUIs in Joe.
      • Optional package.
      • Loaded (with joe.console) by new joe win tool.
      • Widgets
        • Node, Region
        • Pane, StackPane, VBox, HBox
        • Control, Label, Button
    • Added EnumProxy<E> for implementing bindings to Java enums.
    • Added Tuple type, as a tuple return type for functions.
      • The standard catch() function now returns a Tuple rather than a Pair.
      • Removed Pair.
    • Replaced the StringBuilder type with the new TextBuilder type, which is a native type that can be subclassed by Joe classes.
  • Tools

    • Experimental joe win tool
    • joe doc
      • Added @enum entity for documenting enum types.
      • Added @mixin entity for documentation to be included into multiple @type entities
    • joe test
      • Added assertTrue, assertFalse, assertError, and skip functions.
    • joe version
      • New tool, outputs the Joe version and build date.
  • Miscellaneous

    • Removed Joe::codify.
    • Removed the various flavors of Joe::recodify.
    • Added Ant build.xml for building releases.

Changes in 0.3.0 (2024-10-11)

  • Language

    • Numeric literals

      • Can be entered using scientific notation, e.g., 12.34e56
      • Can be entered as hex literals, e.g., 0x12FF.
      • See Types and Values: Numbers.
    • Text blocks

      • Normal strings can no longer contain unescaped newlines.
      • Joe now has Java-like text blocks, enclosed in """ pairs.
      • See Types and Values: Strings.
    • Raw string literals

      • Raw string literals are entered using single quotes rather than double quotes. Within a raw string literal the backslash character has no special meaning.
      • Raw text blocks are entered using ''' rather than """.
    • Operators

      • Added the in and ni membership operators.
      • Added the +=, -=, *=, and /= assignment operators, with their usual semantics.
      • Added the ++ and -- operators, with their usual semantics.
    • Statements

  • Library

    • A value type's type proxy can now inherit methods from the value type's supertype's type proxy.
    • Added the Map type.
    • Added the Set type.
    • Added the AssertError type, which explicitly extends the Error type.
    • Added the StringBuilder type.
    • Added the optional joe.console package, for use by scripts invoked by the command line, including the Path type.
    • Implemented the String.format() method for formatting strings based on a format string, mostly mirroring Java's method of the same name. See String Formatting for details.
    • Added printf() to the Standard Library.
    • Added a printf() method to the StringBuilder type.
  • joe run Tool

  • joe repl Tool

  • joe doc Tool

    • A @type's doc comment can now reference the type's supertype using the @extends metadata tag.
    • @package and @type documentation can include additional topic sections at the bottom of their documentation pages using the new @packageTopic and @typeTopic entries.
  • Documentation

    • Added the relevant documentation for all changes list above.
    • Added the topics "Stringification" and "Iterability" to the Registered Types section.
  • Bug fixes

    • Fixed broken println() function.

Changes in 0.2.0 (2024-09-27)

Joe 0.2.0 extends Joe on every axis.

  • Documentation

    • Added this User's Guide
  • Language

    • Added assert statement
    • Removed print statement (replaced with println(), etc.)
    • + will do string concatenation provided that at least one operand is a string.
    • Added Keyword values: #abc is an interned constant.
    • Added the ternary operator ? :.
    • Joe class declarations may now include static method declarations and static {...} initializer blocks. Static methods and variables are always referenced by the class name, as in Java.
    • Joe functions and methods can now accept a variable length argument list by defining an args parameter as the last parameter in the list. args will be a list containing any excess values.
    • Added lambda functions, e.g., \x -> x*x or \x -> { return x*x; }.
    • Added throw statement.
    • Added break and continue statements.
    • Added foreach statement.
    • JoeError exceptions now include a script-level stack trace, populated as the stack is unwound.
  • Library

    • Added catch() function
    • Added compare() function
    • Added println(), print() functions
    • Added Error proxy (for JoeError and its subclasses)
    • Added Keyword Java type, with proxy
    • Added List Java type, with proxy
    • Added Number proxy (all numbers are Java Doubles)
    • Added Pair Java type, with proxy
    • Added String proxy
  • Embedding API

    • Cleaned up top-level error handling
    • Added Joe::installScriptResource
    • Added TypeProxy<V>, a class that defines a binding for native Java type V. A TypeProxy can provide
      • Instance methods and an initializer for type V
        • E.g., a String's length() method.
      • Static methods and constants
        • E.g., Double.PI, Double.abs().
    • Added notion of a JoePackage, which defines some number of global functions and type proxies.
    • All script-level output now goes through Joe::setOutputHandler, which defaults to writing to System.out.
      • The methods Joe::println and Joe::print can be used from Java bindings.
  • Tools

    • Added joe test, a test runner for Joe-level test scripts, with a aPackage of assertion checkers.
    • Added joe doc, a tool var producing Joe API documentation from scanned "JoeDoc" comments. (See the Library API for examples of joe doc output.)
  • Development

    • Added joe/tests, the script-level Joe test suite.
  • Misc

    • Added LICENSE and CONTRIBUTING.md

Joe 0.1.0 (2024/09/01)

Joe 0.1.0 is a complete implementation of JLox, with the following changes:

  • Syntax
    • null replaces nil.
    • && replaces and.
    • || replaces or.
    • Strings can include the standard string escapes, which are converted into the usual characters.
      • '\\', \t, \b, \n, \r, \f, \"
    • function replaces fun as the function definition keyword.
    • method is added as the method definition keyword.
    • The extends keyword replaces < in class Sub extends Super {...}.
  • Semantics
    • The <, <=, >, and >= operators can compare strings.
    • A function with no return returns the value of the last statement in the block (which is usually null).
    • An expression statement yields the value of the expression.
    • Thus, running a script returns a value.
  • Embedding API
    • The Joe engine is separated from the App application, so that a client can create Joe instances as needed.
      • The embedding API is still mostly non-existent.
  • Tools
    • The App includes "tool" infrastructure, and supports two tools, accessed as joe run and joe repl.
      • The intent is that a client project can reuse the underlying tools in its own application if desired.
      • joe repl outputs the value of each statement; other than that, the two tools are more or less as described in Crafting Interpreters.

The Joe Language

Joe is a Java-like dynamic scripting language:

  • Meant for adding scriptability to Java applications and libraries.
  • Designed to be comfortable for Java developers.
  • Designed to be easy to extend in Java.

But equally, Joe is designed to be insulated from the larger Java world. Adding a Joe interpreter should not, on its own, add security issues to an application.

A vanilla Joe interpreter:

  • Has no access to the host operating system.
    • Unless the client implements a Java binding for Joe that provides such access.
  • Has no access to the Java standard library.
    • Except insofar as the Joe standard library depends on it.

The client is free to add bindings in Java that do both, as described in the Extending Joe section, and as the joe run and joe repl do by adding the joe.console package.

Joe has its origin in the JLox language from Robert Nystrom's outstanding book and website, Crafting Interpreters, but is by no means identical to JLox. (If you should happen to find Joe useful, please buy a copy of Nystrom's book.)

The following sections describe the language in more detail.

Types and Values

In theory, every Java value, without exception, can be a valid Joe value. In practice, Joe provides particular support for the following types:

Booleans

Boolean values are represented by the familiar constants true and false, and internally as Java Booleans.

In boolean expressions, false and null count as false; all other values are considered "truthy".

Numbers

All numbers are represented as IEEE double precision values, i.e., Java Doubles, as in Javascript and Lua.

Syntactically, numeric literals are defined as in Java.

  • 123
  • 123.456
  • 123.4e-56
  • 0x12FF

Strings

Joe strings are represented internally as Java String values, and provide much the same range of operations.

String literals are double-quoted, as in Java:

  • "abcd"

String literals support the usual array of escapes, including \n, \\, \", \t, \b, \r, \f and unicode escapes.1

In addition, Joe supports multiline text blocks delimited by """, somewhat like Java's text blocks:2

var block = """
    Line 1
      "Line 2"
           Line 3
    """;

Text blocks follow these rules:

  • Leading blank lines are stripped.
  • Trailing whitespace is stripped
  • What remains is outdented to the left margin via Java's String::stripIndent() method, which preserves relative whitespace.
  • Text blocks can contain all the usual escapes, including \".
  • But single " characters in a text block need not be escaped.

Thus, the block shown above would print like this:

Line 1
  "Line 2"
      Line 3

Raw String Literals

A string entered using single quotes is a raw string literal. In a raw string literal the backslash is just another character; the literal '\n' yields a string containing a backslash followed by a lower-case n.

Similarly, a text block contained within ''' delimiters is a raw text block, and is processed in the same way: as a normal text block, but without any backslash escapes.

The primary use for raw strings is to make regular expressions more readable.

Keywords

A Keyword is an interned symbol, implemented internally using Joe's Keyword class. Keywords are frequently used where a Java program would use enumerations.

A keyword literal is an identifier preceded by a # character:

  • #yes
  • #no
  • #descending

Lists

A List is a Java List<Object> that contains an ordered collection of arbitrary Joe values, and has much the same operations as Java lists. See the link for the full API.

There are several ways to create and initialize lists. First, build it up one element at a time:

var list = List();
list.add("a");
list.add("b");
list.add("c");
list.add("d");

Or use the Java-like List.of(items...) method:

var list = List.of("a", "b", "c", "d");

However, lists are usually created using Joe's list literal syntax:

var list = ["a", "b", "c", "d"];

List literals are allowed to have a trailing comma:

var list = [
    "this",
    "that",
    "the other",
];

Lists can be queried much as in Java:

var list = ["a", "b", "c", "d"];

list.isEmpty();      // False
list.size();         // 4
list.get(1);         // "b"

List items can also be accessed using array notation:

var list = ["a", "b", "c", "d"];

var c = list[2];    // list.get(2)
list[3] = "xyz";    // list.set(3, "xyz");

Indices are zero-based, and must refer to an existing item.

Maps

A Map is a Java Map<Object,Object>, key/value store. It has much the same operations as Java maps. See the link for the full API.

There are several ways to create and initialize lists. First, build it up one entry at a time:

var map = Map();
map.put(#a, 1);
map.put(#b, 2);
map.put(#c, 3);

Or use the Java-like Map.of(values...) method:

var map = Map.of(#a, 1, #b, 2, #c, 3);

However, maps are usually created using Joe's map literal syntax:

var map = {#a: 1, #b: 2, #c, 3};

Map literals are allowed to have a trailing comma:

var map = [
  #a: 1,
  #b: 2,
  #c: 3,
];

Maps can be queried much as in Java:

var map = {#a: 1, #b: 2, #c, 3};

map.isEmpty();      // False
map.size();         // 3
map.get(#b);        // 2

Maps can also be accessed using array notation:

var map = {#a: 1, #b: 2, #c, 3};

var c = map[#c];   // map.get(#c);
map[#d] = 4;       // map.put(#d, 4);

Sets

A Set is a Java Set<Object>. It has much the same operations as Java sets. See the link for the full API.

var set = Set(#a, #b);
println(set.contains(#b)); // Outputs "true".

Errors

An Error is a Java JoeError exception: a Joe runtime error, an error thrown explicitly by a Joe script, or a Joe assertion failure.

Errors are thrown using the throw or assert statements, and can be caught using the catch() function.

Functions and Methods

Joe's functions and methods are first class values. They can be assigned to variables, passed to functions, and invoked at a later time. See the sections on Functions and Classes for more information.

Classes

Joe scripts can define classes; a class can have:

  • An initializer
  • Instance variables
  • Instance methods
  • Static variables
  • Static methods
  • A static initializer

Joe classes support single inheritance.

See the section on Classes for more information.

Records

Joe scripts can define record types. Like Java record types, a Joe record has an ordered number of immutable fields, and can have:

  • Instance methods
  • Static variables
  • Static methods
  • A static initializer

See the section on Records for more information.

1

Joe supports Unicode escapes, e.g., \u1234, as in Java; but only within string literals.

2

The documentation for Java's text blocks is strewn with special cases, and is nearly impenetrable. The Joe implementation is simpler and a little more forgiving, relying on Java's String::stripIndent to do most of the work; and I think the result is pretty much the same.

Operators

Joe defines a subset of Java's operators, with a few differences.

Arithmetic Operators

The +, -, *, and / operators (including unary -) are defined as in Java, with the usual precedence.

The + operator provides string concatenation instead of numeric addition if at least one operand is a String.

Comparison Operators

The == and != compare for equality using the logic of Java's Objects.equals(); thus, they can be used to compare any two Joe values for equality. As a result, Joe values do not provide an equals() method.

The >, <, >=, and <= operators compare numbers as they do in Java, but also compare Strings lexicographically.

Membership Operators

The in and ni ("Not In") operators check for membership of elements in collections, e.g., Joe List values.

var list = List("A", "B", "C");

if ("A" in list) println("Got it!");
if ("D" ni list) println("Nope, not there!");

These operators work with the same set of collection values as the foreach statement.

Matching Operator

The ~ operator matches a value against a destructuring pattern, returning true or false. For example, here it used to determine whether myValue contains a three-item list whose first item is #fred:

if (myValue ~ [#fred, _, _]) {
    println("Match!");
}

If the pattern contains binding variables, the variables are implicitly declared within the current scope. On a successful match they are set to the matching values within the target; on failure they are set to null:

if (myValue ~ [#fred, height, weight]) {
    println("Fred is " + height + " inches tall,");
    println("and weighs " + weight + " pounds.");
}

See the Pattern Matching section for more about pattern matching and Joe's pattern syntax.

Logical Operators

The && and || operators provide short-circuit execution of Boolean expressions in the usual way. However, in Joe the values false and null are considered false in Boolean expressions, and all other values are considered true.

Thus, instead of yielding true or false, && and || yield the last operand to be evaluated. Consider:

var x = 5;
var y = null;

println(x || 1); // Prints 5
println(y || 1); // Prints 1

The ! operator negates a Boolean expression, yielding true or false.

The Ternary Operator

The ternary (? :) operator is defined as in Java.

var x = 100;
println(x > 50 ? "big" : "small"); // Prints "big"

Assignment Operators

The =, +=, -=, *=, and /= operators work essentially as they do in Java.

In addition, the += operator concatenates strings if either the left or right-hand side is a string, just as + does.

x = y = 5;  // Assigns 5 to x and y.

Pre- and Post-Increment/Decrement Operators

The ++ and -- operators work as they do in Java.

The @ Operator

The @ operator is used in instance methods as a synonym for this.. See Classes for more details.

Statements

The Joe language provides the following statements. Statements are terminated by a semicolon or (sometimes) a block, as in Java.

Variable Declarations

All variables must be declared before use using the var statement. The var statement can assign an initial value; if the initial value is omitted, the variable is initialized to null.

var x = 5;
var y;      // y == null

Joe is lexically scoped; undeclared variables in function or method bodies are presumed to be declared in an enclosing scope. It is an error if they are not.

In addition to declaring and initializing individual variables, the var statement can use a pattern to perform a destructuring bind: that is, to bind one or more variables to values within a data structure. For example, suppose that the function f() returns a two-item list, and the caller wants to assign the list items to the variables x and y.

One could do this:

var result = f();
var x = result[0];
var y = result[1];

Or, one could do a destructing bind:

var [x, y] = f();

Here, var matches the list returned by f() against the pattern [x, y] and binds variables x and y to the matched values.

When the pattern doesn't match: If var's pattern doesn't match the target value, e.g., if f() didn't return a two-item list in the example shown above, then the pattern match will fail and Joe will throw a runtime error. Therefore, var should only be used when the shape of the target value is known ahead of time. Use the ~ operator or the match statement to test whether a value matches a particular pattern.

See Pattern Matching for more on pattern matching and destructuring binds, including Joe's full pattern syntax.

Function Declarations

Functions are declared with the function statement. See Functions for more details.

Class Declarations

Classes are declared with the class statement. See Classes for more details.

Expression Statements

Any expression becomes a statement when followed by a semicolon.

x + y;
doSomething();

To execute an expression and see its value in joe repl, enter the expression as an expression statement.

Blocks

A block is a sequence of statements enclosed in curly brackets, as in Java. Each block defines a lexical scope.

var x = 5;
{
    var x = 6;
    println(x); // Prints "6"
}
println(x);     // Prints "5"

Return

The return statement is used to return from functions and methods. As in Java, it takes an optional expression to return a value.

When used at script level, return terminates the script, optionally returning a value. This value is displayed in the REPL and is accessible via Joe's embedding API, but is not displayed by joe run.

If Statements

if statements are defined as in Java.

if (x == 5) {
    ...
} else if (x == 15) {
    ...
} else {
    ...
}

While Loops

while loops are defined as in Java.

var x = 0;
while (x < 10) {
    ...
    x = x + 1;
}

For Loops

for loops are defined as in Java, except that for (var item : list) is not supported. See Foreach Loops, below.

for (var i = 0; i < 10; i = i + 1) {
    ...
}

Foreach Loops

foreach loops allow iteration over the members of a collection, e.g., a Joe List.

var list = ["a", "b", "c"];

// Prints "a", "b", and "c" on successive lines.
foreach (item : list) {
    println(item);
} 

In addition, foreach can use a pattern to do a destructuring bind on each list item:

var list = [[#a, 1], [#b, 2], #whoops, [#c, 3]];

// Prints #a, #b, and #c on successive lines
foreach ([x, _] : list) {
    println(x);
}

foreach will silently ignore any list items that don't match the pattern, making it a useful tool for extracting data from homogeneous lists.

See Pattern Matching for more on pattern matching and destructuring binds, including Joe's full pattern syntax.

Break and Continue

The break and continue statements break out of the enclosing loop or continue with the next iteration of the enclosing loop, just as in Java.

There is no support for labeled breaks or continues in order to jump through nested loops.

Switch Statements

Joe's switch statement uses Java's enhanced switch syntax rather than the classic C-like syntax:

switch (x) {
    case 1, 2, 3 -> return "abc";
    case 4, 5, 6 -> return "def";
    case 7, 8, 9 -> return "ghi";
    default -> return "???";
}
  • The switch value and the case values can be any Joe value or expression.
  • There must be at least one case clause.
  • Each case can have one or more values to match.
  • Each case's body can be a single statement or a block.
  • If present, the default clause must follow all the case clauses.

The implementation is quite simple: each case value is checked in turn, from the top to the bottom, until a match is found; then the case's body is executed.

Match Statements

The match statement is similar to a switch statement, but matches patterns against a target value instead of checking for equality. It is especially useful for processing a heterogeneous list of values.

match (value) {
    case [a, b] -> 
        println("Two item list of " a + " and " + b + ".");
    case Person(name, age) -> 
        println("Person " + name + " is " + age + " years old.");
    default -> println("no match");
}

Every match statement requires at least one case; the default case is optional. (Note that matching on _, a simple wildcard pattern, is equivalent to the default case:

match (value) {
    case [a, b] -> 
        println("Two item list of " a + " and " + b + ".");
    case Person(name, age) -> 
        println("Person " + name + " is " + age + " years old.");
    case _ -> println("no match");
}

Each case in a match can include an optional guard clause that adds a boolean guard condition on top of the pattern match. In the following example the case matches any Person record, and then requires that the person be at least 10 years old.

match (value) {
    ...
    case Person(name, age) if age >= 10 -> 
        println("Person " + name + " is at least 10 years old.");
    ...
}

See Pattern Matching for more on pattern matching and destructuring binds, including Joe's full pattern syntax.

Throw

The throw statement is used to explicitly throw error exceptions in a Joe script.

if (x <= 0) {
    throw Error("Expected a positive number.");
}

For convenience the message string can be thrown directly; the following code is equivalent to that above:

if (x <= 0) {
    throw "Expected a positive number.";
}

Thrown errors can be caught using the catch() function; and once caught can be rethrown.

Assert

The assert statement checks a condition and throws an error if it is unmet. Assertions are always checked; they are not disabled in production as in Java.

assert x > 0;

The programmer may provide an optional message.

assert x > 0, "x must be positive!";

The various checkers and assertion functions defined in joe test's Test API all work in terms of the assert statement.

Functions

Joe functions are declared using the function statement.

function square(x) {
    return x*x; 
}

Use the return statement to return early from a function, or to return a value.

Variable Length Argument Lists

To write a function that takes a variable length argument list, use the args parameter. args will accept zero or more arguments, which it will present to the function as a Joe List.

function howdy(greeting, args) {
    foreach (var person : args) {
        println(greeting + ", " + person + "!");
    }
}

// Says hello to Joe, Bob, Dave, and Ted
howdy("Hello", "Joe", "Bob", "Dave", "Ted");

If used, args must be the last argument in the list.

Function References

Joe functions are first-class values, and can be passed to functions, assigned to variables, and then called.

function square(x) { return x*x; }

var myFunc = square;

println(myFunc(5)); // Prints "25".

Nested Functions

Functions can be declared in any scope, not just at the global scope, and are visible only in that scope.

function a() {
    function b() { println("In function b."); }
    b(); // Prints "In function b."
}
b(); // Throws an unknown variable error.

Lambda Functions

Joe supports lambda functions, i.e., unnamed function values. The syntax for defining a lambda function is:

  • \ <params> -> <expression>
  • \ <params> -> { <statements> }

For example, the following are all equivalent:

// A normal function called "lambda"
function lambda(x) { return x*x; }

// A lambda that squares a value
var lambda = \x -> x*x;

// The same lambda, expressed as a block
var lambda2 = \x -> { return x*x; };

The advantage of lambdas is that they can be passed directly to functions without having to define a name:

var list = List(1,2,3,4,5);

// Compute a list of the squares of the numbers in the original list.
var squares = list.map(\x -> x*x);

Classes

Joe supports a simple notion of classes, including single inheritance.

class Person {
    method init(first, last) {
        this.first = first;
        this.last = last;
    }
    
    method fullName() {
        return this.first + " " + this.last;
    }
}

var joe = Person("Joe", "Pro");
println(joe.first);         // Prints "Joe"
println(joe.fullName());    // Prints "Joe Pro"

Class Properties

A class has two kinds of property, method properties and variable properties, aka instance variables.

The two kinds of property share a namespace. Take care not to give instance variables the same name as methods; the variable will shadow the method, yielding surprising results.

Variable Properties

Instance variables are accessed via the this variable within the class's methods, and as properties of the instance in other scopes:

class Person {
    ...
    method fullName() {
        return this.first + " " + this.last;
    }
}

var joe = Person("Joe", "Pro");
println(joe.first);         // Prints "Joe"

Method Properties

Methods are defined in the class body using the method statement.
Methods are identical to Joe functions, except that each method is bound to its instance and has access to its instance via the this variable.

Methods are accessed via the this variable within the class's methods, and as properties of the instance in other scopes:

class Person {
    method fullName() { ... }
    method greet() { println("Howdy," + this.fullName()); }
}

var joe = Person("Joe", "Pro");
joe.greet();    // Prints "Howdy, Joe Pro!"

The Class Initializer

The class's name, which by convention begins with an uppercase letter, serves as the class's constructor. It delegates its work to the class's optional init method, and takes the same arguments.

class Person {
    method init(first, last) {
        this.first = first;
        this.last = last;
    }
    ...    
}

var joe = Person("Joe", "Pro");

The init method can be called directly on an instance to reinitialize the object, but this is rarely done.

The @ Operator

Joe requires that methods refer to all instance variables and methods using this., which yields cluttered, noisy looking code. As a convenience, the @ operator is equivalent to this., enabling code like this:

class Person {
    method init(first, last) {
        @first = first;
        @last = last;
    }
    
    method fullname() {
        return @first + " " + @last; 
    }
    
    method greet(greeting) {
        return greeting + ", " + @fullname() + "!";
    }
}

Method References

An instance method is a first class object, just like a function, and can be referenced by its name. For example,

class Person {
    ...
    method fullName() {
        return this.first + " " + this.last;
    }
}

var joe = Person("Joe", "Pro");
var myFunc = joe.fullName;
println(myFunc());          // Prints "Joe Pro"

Superclasses and Inheritance

A class can inherit from a superclass using the extends keyword:

class Superclass {
    method name() { return "Superclass"; }
}

class Subclass extends Superclass {
    method name() {
        return "Subclass of " + super.name();
    }
}

var sub = Subclass();
println(sub.name());    // Prints "Subclass of Superclass".

As shown in the example, a subclass can call superclass methods by means of the super variable. super is only defined in methods of subclasses.

Records

Joe supports record types, similar to Java's record types.

record Person(first, last) {
    method fullName() {
        return this.first + " " + this.last;
    }
    ...
}

var joe = Person("Joe", "Pro");
println(joe.first);         // Prints "Joe"
println(joe.last);          // Prints "Pro"
println(joe.fullName());    // Prints "Joe Pro"
println(joe);               // Prints "Person(Joe, Pro)"

Like a Java record, a Joe record is immutable, having only the fields defined in the record declaration.

In most ways, a record type is just like a class. That is, a record type may have:

  • methods
  • static methods
  • static fields
  • a static initializer

A record type differs from a class in the following ways:

  • Its fields are immutable, and no new fields may be added dynamically.
  • Its fields have a well-defined order.
  • Instances are initialized automatically; no init method is required.
  • The record type's init method, if it has one, has no special semantics; it's just another method.
  • A record type cannot extend another type or be extended by another type.

See the Classes section for more information on methods, etc.

Records and Pattern Matching

Record types are especially useful when pattern matching is used.

Pattern Matching

Joe supports a rich pattern-matching capability inspired by the Rust language's similar capability. Joe's ~ matching operator and the var, foreach, and match statements all make use of pattern matching to do destructuring binds.

Joe supports a rich variety of patterns. Some of them can contain other patterns as subpatterns, allowing a pattern to match quite complicated data structures.

Patterns and Destructuring Binds

A destructuring bind is a way to bind one more variables to values within a complex data structure. The bind makes use of a pattern that duplicates the structure of the target value to match variable names to specific elements.

For example, suppose that the function f() returns a two-item list, and the caller wants to assign the list items to the variables x and y.

One could do this:

var result = f();
var x = result[0];
var y = result[1];

Or, one could do a destructing bind using var:

var [x, y] = f();

Here, var matches the list returned by f() against the pattern [x, y] and binds variables x and y to the matched values.

Binding Variables

We've seen binding variables in most of the examples shown above. A binding variable is a variable name that appears within the pattern and is assigned the corresponding value in the match target.

var [a, b] = [1, 2];    // a = 1, b = 2.

A binding variable can also be used to capture a subpattern. In the following example, the variable b is bound to the list [2, 3] while c and d are bound to the values of the list's items.

var list = [1, [2, 3], 4];

var [a, b@[c, d], e];

Capturing a subpattern is especially useful with foreach: the pattern can capture the entire item if the pattern matches. The following code pulls two-item lists out of a heterogeneous list of values.

foreach (item@[_, _] : inputs) { 
    println(item);
}

If a variable appears in the pattern more than once, it is bound on first appearance and the bound value must match on subsequent appearances.

// flag is true, a == 1
var flag = [a, a] ~ [1, 1];

// flag is false, a == null
var flag = [a, a] ~ [1, 2];

Wildcards

A wildcard is a pattern that matches (and ignores) any value. A wildcard is written as an identifier with leading underscore, e.g., _, _ignore, _x. For example:

var [x, _] = ["abc", "def"];

x will be assigned the value "abc", while the second item of the target list will be ignored.

It's most common to use the wildcard _; but using a longer name can be useful to document what the ignored value is:

var [first, _last] = ["Joe", "Pro"];

Using _last indicates that we don't care about the last name at the moment, but also shows that it is the last name that we are ignoring.

Constants

A constant pattern is a constant value included in the pattern; the corresponding value in the target must have that exact value.

function isPro(list) {
    if (list ~ [_, "Pro"]) {
        return true;
    } else {
        return false;
    }
}

var x = isPro(["Joe", "Pro"]);       // Returns true
var y = isPro(["Joe", "Amateur"]);   // Returns false

The constant must be a literal string, number, boolean, keyword, or null.

Interpolated Expressions

To use a computed value as a constant, interpolate it using $(...).

var a = 5;
var b = 15;

var [x, $(a + b)] = [10, 20];  // Matches; x == 10.

Here, $(a + b) evaluates to 20, which matches the second item in the target list.

The parentheses may be omitted if the interpolated expression is just a variable name:

var wanted = "Pro";

var [first, $wanted] = ["Joe", "Pro"];   

List Patterns

We've seen many list patterns in the above examples. Syntactically, a list pattern is simply a list of patterns that matches a List of values. The matched list must have exactly the same number of items as the list pattern, and each subpattern must match the corresponding item.

if (list ~ [a, [b, _], "howdy"]) {
    // ...
}

The pattern [] matches the empty list.

Sometimes the length of the list is unknown; in this case, the list pattern can provide a pattern variable to bind to the list's tail:

if (list ~ [a, b : tail]) {
    // tail gets the rest of the list.
}

The variables a and b will get list[0] and list[1], and tail will get any remaining items, or the empty list if list.size() == 2. (The match will naturally fail if list.size() < 2.)

Map Patterns

A map pattern matches objects with keys and values, e.g., Map values.

  • The keys must be constants
  • The values can be any pattern.
  • The target Map must contain all of the keys listed in the pattern, and their values must match the corresponding value patterns.
  • The target Map can contain any number of keys that don't appear in the pattern.

Some examples:

var {#a: a, #b: b} = {#a: 1, #b: 2, #c: 3};  // a = 1, b = 2
var ($x: value} = someMap;                   // value = someMap.get(x)
var {#a: [a, b, c], #b: x} = someMap;

Matching Instances with Map Patterns

A map pattern can also match any Joe value with field properties, e.g., an instance of a Joe class. The pattern's keys match the field names and the pattern's values match the field values.

  • Key patterns must be string or Keyword constants that correspond to the field names, or interpolated expressions that evaluate to such strings or keywords.
class Thing {
    method init(id, color) {
        this.id = id;
        this.color = color;
    }
}

// These two statements are equivalent
var {"id": i, "color": c} = Thing(123, "red");
var {#id: i,  #color: c}  = Thing(123, "red");

As when matching Map values, the pattern can reference a subset of the object's fields.

Named-Field Patterns

A named-field pattern matches the type and field values for any Joe value with named fields. It will also match a Fact value based on its relation and fields.

class Thing {
    method init(id, color) {
        this.id = id;
        this.color = color;
    }
}

var Thing(id: i, color: c) = Thing(123, "red");

A named-field pattern consists of the name of the desired type, followed by field-name/pattern pairs in parentheses.

  • The named type must be the target value's type or one of its supertypes.
  • The value must have all of the specified fields.
  • The field patterns must match the field values.

Types are matched based on their names, i.e., in var Thing(...) = thing; the type will match if Joe.typeOf(thing).name() == "Thing", not if Joe.typeOf(thing) == Thing. In other words, there is no requirement that the matched type is in scope; it is enough that the value being matched knows its type and that its type's name is the name included in the pattern. See Unscoped Types for more information.

Ordered-Field Patterns

Ordered-field patterns match Joe values with ordered fields, i.e., fields that can be accessed by index as well as by name. Joe records and most Fact values have ordered fields, and proxied types can have ordered fields as well. This allows a stream-lined pattern syntax.

record Person(name, age) {}

var person = Person(Joe, 80);

// These statements are identical
var Person(n, a) = person;               // Ordered-field
var Person(name: n, age: a) = person;    // Named-field

The first form matches the values of the type's fields in sequence. All fields must be represented. The field subpatterns can be any arbitrary patterns, as usual.

Values with ordered fields can also be matched by map patterns and named-field patterns.

Introspection

Introspection, also known as reflection, is the ability for a script to query metadata about its types and values. This section discusses Joe's introspection features, which are available via the methods of the Joe singleton.

Finding a value's type

Given any value whatsoever, Joe.typeOf(value) will return its type.

Given the type, type.name() will return the type's name.

Scoped vs. Unscoped Types

Most Joe types are defined in the global environment as a variable with the type's name whose value is the type itself.

  • E.g., the standard type String is referenced by the global variable String.

A Joe class is defined in the scope that contains the class declaration. If class Thing {...} appears at global scope, it will define a global variable called Thing. If class Thing {...} appears in a local scope, e.g., in a function body, it will define a variable called Thing in that scope.

A type defined in a scope is called a scoped type.

But consider this case:

function makeValue() {
    class MyType { ... }
    return MyType();
}

var theValue = makeValue();

The function makeValue creates a class called MyType in its own scope and then returns an instance of the class. The variable MyType goes out of scope when makeValue returns; the type can no longer be accessed "by name" in any scope in the script. MyType is now said to be unscoped.

And yet, theValue still has type MyType:

// Prints "MyType"
println(Joe.typeOf(theValue).name());

Similarly, just as a client can extend Joe with a binding for a scoped type (in exactly the same way as standard types like String and List), a client can also create values of named-but-unscoped types.

Unscoped Types and Pattern Matching

Joe's pattern matching feature provides several ways to match on a value's type. In each case, the type is matched by its name rather than by strict equality of type objects, precisely because the value's type might be unscoped: the type's name is known to the value and to the script author, but isn't assigned to any convenient variable.

In other words,

if (thing ~ Thing{#id: id}) {
    ...
}

will match if Joe.typeOf(thing).name() == "Thing". Whether type Thing's variable is in-scope is irrelevant.

Field Names

The Joe.getFieldNames(value) method will return a list of the value's fields. For example,

class Thing {
    method init(id, name) {
        this.id = id;
        this.name = name;
    }
}

// Prints ["id", "name"]
println(Joe.getFieldNames(Thing));

If the given value is a Joe type, Joe.getFieldNames() will return the names of the type's static fields, not the names of its instance fields.

It may seem odd to call Joe.getFieldNames() on a value to discover the value's fields, rather than on the value's type; but Joe types don't generally know the names of their instances' fields. Consider the following example:

class Person { 
    method init(name) {
        this.name = name; 
    } 
}

var fred = Person("Fred");
fred.favoriteFlavor = "chocolate";
fred.lastYearsFavorite = "vanilla";

Every Person is assigned a name in the initializer; but the script author has decided to add two additional fields to Person fred. A script can add any number of fields to any class instance, fields other instances of that type won't have.

In fact, the Person type doesn't even know that every Person has a name. The init() method could be written to assign a value to this.name for some persons but not others.

Thus, there's no way to query type Person to find the field names of its instances.

Nero Datalog

Nero is Joe's own dialect of the Datalog relational query language. Datalog is often used as a query language for databases; Joe provides it as a means of querying and transforming in-memory collections of data.

Joe provides Nero in two forms:

  • As a standalone language, via the joe nero tool.
  • As a language embedded in Joe scripts.

The following subsections discuss these in turn.

  • The Nero Tutorial describes Nero as a standalone language.
  • Embedded Nero describes how Nero is used within a Joe script, and particularly how to use scripted input facts and how to produce outputs in the script's domain types.
  • 'ruleset' Reference is a concise statement of the syntax and semantics of Nero rule sets.
  • Technical Details places Nero within the larger family of Datalog implementations, and describes specific differences from "standard" Datalog.

Nero Tutorial

This section describes the Nero language as a standalone language. Nero in this form can be used via the joe nero tool.

Standalone Nero, as provided by joe nero, is useful in these ways:

  • To experiment with and learn Nero syntax.
  • To prototype Nero rule sets for particular purposes.
  • As a test bed for Nero's Datalog implementation.

Axioms and Rules

A Nero program consists of a set of axioms and rules. Axioms assert facts and rules derive new facts from known facts. These derived or inferred facts constitute the output of the program.

For example the following program asserts the parent/child relationships between certain individuals, and gives rules for determine ancestors and their descendants. (Comments begin with // and continue to the end of the line, as in Joe.)

// Parent/2 - parent, child
Parent(#anne, #bert);
Parent(#bert, #clark);

// Ancestor/2 - ancestor, descendant
Ancestor(x, y) :- Parent(x, y);
Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);

The axiom Parent(#anne, #bert) asserts the logical predicate that #anne is the Parent of #bert.

  • Parent is called the relation.
  • #anne and #bert are constant terms representing individuals.
    • A literal constant term in a Nero program must be one of the following:
      • A Joe keyword, string, number, true, false, or null.
  • The string Parent/2 appearing in the comment indicates that Parent is a relation with an arity of 2, i.e., it always takes two terms.
  • A form consisting of a relation and one or more terms is called an atom.

The collection of Parent axioms is essentially equivalent to a table with two columns in a relational database.

The rule Ancestor(x, y) :- Parent(x, y); simply asserts that if some individual x is the parent of some individual y, then x is also an ancestor if y.

  • Ancestor is thus the relation of a new collection of predicates.
  • The single atom to the left of the :- token is called the head of the rule.
  • The atom(s) to the right of the :- token are called the body of the rule
  • x and y are called variable terms. In Nero, variable terms are just normal Joe identifiers.

Similarly, the rule Ancestor(x, y) :- Parent(x, z), Ancestor(z, y); simply asserts that if x is the parent of z, AND z is an ancestor of y, then x must be an ancestor of y.

A Nero program can contain any number of axioms and rules, making use of any number of distinct relations.

Execution

Executing a Nero program amounts to iterating over the rules, matching the body of each rule against the known facts. For each successful match, Nero infers a new fact using the rule's head. This process continues until no new facts are produced.

For the simple program we've been looking at,

// Parent/2 - parent, child
Parent(#anne, #bert);
Parent(#bert, #clark);

// Ancestor/2 - ancestor, descendant
Ancestor(x, y) :- Parent(x, y);
Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);

we get the following output.

Ancestor(#anne, #bert)
Ancestor(#anne, #clark)
Ancestor(#bert, #clark)
Parent(#anne, #bert)
Parent(#bert, #clark)

The Parent facts are inferred trivially from the axioms; and then the Ancestor facts are inferred from the Parent facts.

Notice how the variables are used in the second rule:

  • First, some Parent fact is found and the variable terms x and z are bound to the parent and child in that fact.
  • Then, some Ancestor fact is found in which individual z is the ancestor; y is then bound to the descendant.
  • The bound values of x and y are then inserted into Ancestor(x, y) to produce the new Ancestor fact.

Wildcards

Suppose we wanted to produce a list of all the individuals mentioned in our input database. We could use a rule like this:

Person(x) :- Parent(x, y);
Person(x) :- Parent(y, x);

That is, x is a person if x is either a parent or a child in a parent/child relationship.

Notice that the x variable does all the work in these two rules. The y variable matches something, but we never care what. In cases like this we usually use wildcard terms instead of variable terms:

Person(x) :- Parent(x, _);
Person(x) :- Parent(_, x);

A wildcard term can match anything at all; syntactically, a wildcard is simply an identifier beginning with an underscore, usually just the bare _. A rule's body can contain any number of wildcards, each of which matches something different.

It's desirable to use wildcards in cases like these, especially in more complicated rules, as it explicitly says that "this term doesn't matter".

Negation

The rules we've seen so far look for matches with known facts; it's also possible to look for an absence of matches. For example, suppose we want to identify the individuals in the Parent data who have no known parent. We could add a rule like this:

Progenitor(x) :- Parent(x, y), not Parent(_, x);

This says that x is a "progenitor" is x is the parent of some individual y but there is no individual in the input data that is the parent of x.

This technique is called negation, and the body atom following the not token is said to be negated.

A negated atom can only refer to variables bound in the non-negated atoms to its left; because a negated atom matches an absence of facts, not a specific fact, it cannot bind new variables. Consequently, we have to use a wildcard for the first term in not Parent(_, x) rather than a variable. (In practice, we'd probably use a wildcard for the y term in Parent(x, y) as well.)

Using negation requires that the rule set be stratified; Nero will flag an error if it is not. This is not usually a problem in practice; see the discussion of stratification in the Technical Details section if the error rears its ugly head.

Constraints

Barring time travel, it is impossible for a person to be his or her own ancestor; if our graph of parents and children contains a cycle, that implies an error in our data. We can write a rule to detect such errors by using a constraint:

CycleError(x) :- Ancestor(x, y) where x == y;

We flag person x as an error if x is his own ancestor.

A constraint is a simple comparison consisting of:

  • A variable term, bound in a body atom.
  • A comparison operator, one of ==, !=, >, >=, <, <=.
  • A second term, either a constant term or another variable term bound in a body atom.
CycleError(x) :- Ancestor(x, x);

The rule will fire for any Ancestor fact whose first and second terms are the same. This shows an important principle: when matching facts, Datalog binds a variable to a value on first appearance from left to right. Subsequent appearances must match the bound value.

Embedded Nero

This section explains how to use Nero within Joe scripts.

A Simple Rule Set

A Nero program is embedded in a Joe script using the ruleset expression. The following declaration defines a RuleSet called myRules. The content of the expression's body is just a Nero program, as described in the tutorial, possibly augmented with export declarations.

var myRules = ruleset {
    // Parent/2 - parent, child
    Parent(#anne, #bert);
    Parent(#bert, #clark);

    // Ancestor/2 - ancestor, descendant
    Ancestor(x, y) :- Parent(x, y);
    Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
};

A RuleSet provides a number of ways to infer the facts resulting from the rules; the simplest is infer(), which simply executes the rule set as is, given any defined axioms, and returns the resulting facts.

foreach (fact : myRules.infer()) {
    println(fact);
}

This will output the following:

Fact(Ancestor, #anne, #bert)
Fact(Ancestor, #bert, #clark)
Fact(Parent, #anne, #bert)
Fact(Parent, #bert, #clark)
Fact(Ancestor, #anne, #clark)

The 'Fact' Type

By default, all new facts inferred by an embedded Nero rule set are of type Fact. A Fact consists of a relation, e.g., "Ancestor", and a list of terms, e.g., #anne and #bert.

In most ways a Fact value can be treated like an ad hoc Joe record value, i.e., the same pattern that will match a record of type Ancestor will also match a Fact whose relation is "Ancestor":

foreach (Ancestor(a, d) : myRules.infer()) {
    println(a + " is an ancestor of " + d);
}

When inferred facts are going to be used immediately, e.g., to produce output, the Fact type is often sufficient. However, it is also possible to make the rule set produce outputs using the script's own defined types; see Exported Domain Values, below.

Scripted Input Facts

A Nero rule set can include axioms, as shown above, but usually it gets its input facts from the script. Any Joe value with named fields can be used as an input fact. This includes instances of:

  • The Fact type.
  • Any Joe record type.
  • Any joe class.
  • Native types whose proxies define fields.
  • Any native type whose proxy explicitly overrides isFact and toFact to do something sensible in context.

The type's type name is used as the input fact's relation, and its fields as the fact's terms.

The scripts a list or set of all of the input facts, and passes it to the rule set's infer() method.

record Parent(parent, child) {}

var inputs = [
    Parent(#anne, #bert),
    Parent(#bert, #clark)
];

var myRules = ruleset {
    // Ancestor/2 - ancestor, descendant
    Ancestor(x, y) :- Parent(x, y);
    Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
};

foreach (fact : myRules.infer(inputs)) {
    println(fact);
}

Input Facts without Ordered Fields

Class instances do not have ordered fields the way Fact values and record values do. Therefore, the usual rule syntax that depends on ordered fields will not work:

class Parent {
    method init(parent, child) {
        @parent = parent;
        @child = child;
    }
}

var myRules = ruleset {
    // Parent(x, y) cannot match an instance of class parent!
    Ancestor(x, y) :- Parent(x, y);
    Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
};

Consequently, we must use named-field atoms for class instances and other types with named fields but not ordered fields:

var myRules = ruleset {
    Ancestor(x, y) :- Parent(parent: x, child: y);
    Ancestor(x, y) :- Parent(parent: x, child: z), Ancestor(z, y);
};

Named-field atoms also allow a rule to match on a subset of a fact's fields, whether ordered or not.

Exported Domain Values

As noted above, a rule set outputs inferred facts using the Fact type. This is sufficient in many cases; but it is also common to want the outputs to be of types defined by the script itself. Accomplish this by adding export declarations to the rule set.

The simplest export declaration simply states that a given relation has a matching domain type in the current scope. When rule set execution is complete, facts with the exported relation will be converted to values of the named type using the type's initializer.

For example, the following script defines a record type called Ancestor and tells the rule set to export all Ancestor facts using the Ancestor type.

record Ancestor(ancestor, dependent) {}

var myRules = ruleset {
    export Ancestor;
    Ancestor(x, y) :- Parent(x, y);
    Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
};
  • The Ancestor type must be in scope when the rule set is defined.
  • The Ancestor type's initializer must take the same number of arguments as the Ancestor relation in the rule set.

However, it is also possible to export an inferred fact as any kind of value the script's author desires by providing an explicit creation function as a callable reference. For example, suppose the script author wants Ancestor facts returned as two-item lists:

function ancestor2list(x, y) {
    return [x, y];
}

var myRules = ruleset {
    export Ancestor as ancestor2list;
    Ancestor(x, y) :- Parent(x, y);
    Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
};

A lambda function could also be used:

var myRules = ruleset {
    export Ancestor as \x,y -> [x, y];
    Ancestor(x, y) :- Parent(x, y);
    Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
};

The only restriction is that the callable referenced by the as clause is defined in the scope in which the rule set is declared.

'ruleset' Reference

A Nero rule set is defined within a Joe script using the ruleset expression, which returns a RuleSet value.

The ruleset Expression

A ruleset expression has this form:

ruleset { body }

where body consists of Nero axioms and rules, just like a standalone Nero program, but may also contain any number of export declarations.

Axioms

An axiom has this form:

relation (term, [term...]);

where

  • relation is an identifier, conventionally beginning with an uppercase letter.
  • The terms are constant terms: Joe keywords, strings, numbers, true, false, or null.

Rules

A rule has this form:

head :- bodyAtom [, bodyAtom...] [where constraints] ;

The rule head is an atom whose terms are constant or variable terms:

relation (term, [term...])

  • All variable terms in the head must be bound in the rule's body.

A body atom is an atom, possibly negated, whose terms may be constants, variables, or wildcards:

[not] relation (term, [term...])

  • The first body atom must not be negated.
  • Variables used in negated atoms and constraints must be bound by non-negated atoms to their left in the rule's body.

A rule may have zero or more optional constraints using the where clause:

where constraint [, constraint...]

A constraint has this form:

variable op term

where

  • variable must be a variable bound in a non-negated body atom.
  • op is a comparison operator, one of: ==, !=, >, >=, <, <=
  • term is a constant or variable term; if a variable, it must be bound in a non-negated body atom.

Export Declarations

By default, all facts inferred via axioms or rules are returned to the script as Fact values. An export declaration directs Nero to return all facts having a given relation as some other kind of Joe value.

An export declaration has this form:

export relation [as expression];

where

  • relation is a relation used in an axiom or in the head of a rule.
  • If no as clause is given, relation must also be the name of a type that is in scope whose initializer takes the same number of arguments as the relevant atoms have terms.
  • If the as clause is given, expression must be a Joe expression that evaluates to a callable in the current scope.

However specified, the callable will be used to convert all facts having the given relation to Joe values on output from the rule set.

Technical Details

This section contains details about Nero and its implementation for those familiar with other implementations of Datalog, and for those interested in the implementation and maintenance of Nero itself.

Nero vs. "Standard Datalog"

Semantically, Nero is a standard implementation of Datalog implemented using fixpoint semantics and the "naive" solution algorithm, augmented with:

  • Stratified negation
  • Constraints
  • The ability to use arbitrary Joe values as input facts.
  • The ability to export inferred facts as values of particular Joe types.

Nero's syntax and conventions have been modified from standard Datalog to better support integration into Joe scripts.

  • Horn clauses are terminated by ; rather than ..
  • Relation names are uppercase by convention, to follow Joe's convention for type names.
  • Nero variable terms are simple identifiers, lowercase by convention.
  • Nero constant terms can be any of the following Joe literal values:
    • In rules and axioms: keywords, strings, numbers, true, false, and null.
    • In scripted input facts: any Joe value
  • Nero body atoms can include wildcard terms, which are written as identifiers with a leading underscore. A wildcard matches any value and creates no binding.
  • Rule constraints appear at the end of the rule, prefixed with where.
  • Rule body atoms can match named fields in the manner of Joe's named-field patterns.

Stratified Negation

A Nero program uses rules to produce new facts. Those rules are allowed to be recursive: relation A can depend on facts of type B, while simultaneously relation B can depend on facts of type A, either directly or indirectly. For example, the following program says that A is true of x IFF B is true of x.

// OK
A(x) :- B(x);
B(x) :- A(x);

But when negation is added, this changes. Consider this program:

// BAD
B(#anne);
A(x) :- C(x), not B(x);
B(x) : A(x);

This will infer:

  • A(#anne) because we have C(#anne) and no B(#anne)
  • B(#anne) because we have A(#anne)
  • But now we have both A(#anne) and B(#anne), which contradicts the first rule. This is the Datalog equivalent of a memory corruption error in other languages.

To prevent this kind of problem, Nero (like many Datalog engines) requires that every ruleset meets the following condition:

  • If relation A depends on relation B with negation, directly or indirectly, relation B cannot depend on relation A, directly or indirectly.

To prevent this, Nero uses a technique called stratified negation, in which the head relations in the rule set are divided into strata such that if relation A depends on relation B with negation, the rules that infer B are all in a lower strata than the rules that infer A. If the rules cannot be so stratified, then the program is rejected.

In this case, look for the kind of circular dependency with negation shown above.

References

Embedding Joe

This section explains how to embed a Joe interpreter into an application or library.1

There are several basic tasks.

Creating the Interpreter

To create an interpreter, just create an instance of Joe.

var joe = new Joe();

That's all there is to it.

Installing Bindings

Most use cases will involve defining domain-specific bindings, as described in the Extending Joe section. Use the following Joe methods to install bindings into your instance of Joe:

  • setVar() to set a variable in the global environment.
  • installGlobalFunction() to install a native function.
  • installType() to register a type proxy for a native type
  • installScriptResource() to load a Joe script from the project jar file.
  • installScriptResource() to load a Joe script from the project jar file.

See the above link and Joe's javadoc for details on how to create these various kinds of bindings.

Executing Scripts

Joe provides two ways to execute scripts:

  • joe.runFile() reads a script from a named file and executes it.
  • joe.run() executes a script passed in as a string.

Each can throw two Joe-specific exceptions.

Joe throws SyntaxError if there's any error found while parsing the script. The exception's message is fairly vanilla; but the exception includes a list of "details" describing all syntax errors found, by source line. SyntaxError::getErrorsByLine() returns the list; SyntaxError::printErrorsByLine() will print the errors to either System.out (as joe run does) or to the PrintStream of your choice.

Joe throws JoeError for any runtime error. There are three subclasses.

  • RuntimeError is an error thrown by Joe's interpreter itself.
    RuntimeError exceptions will usually include the source line number at which the error occurred; see the line() method.
  • AssertError is an error thrown by Joe's assert statement. The message details the condition that failed.
  • A vanilla JoeError is an error thrown by a Joe binding or by a Joe script using Joe's throw statement.

All kinds of JoeError can accumulate a script-level stack trace. Use the exception's getFrames() method to get the stack frames (a list of strings); or its getJoeStackTrace() method to get the stack trace in a form suitable for printing.

Executing a REPL

Joe provides joe repl, a tool that invokes an interactive read/eval/print loop on a vanilla Joe interpreter. A project can easily implement its own REPL with its own bindings; see com.wjduquette.joe.tools.ReplTool for an example. It's essentially some I/O code wrapped around the joe.run() call.

Joe doesn't provide a Joe method for invoking the REPL simply because clients users will want to customize it heavily for their needs.

Redirecting Output

The Joe tools, joe run et al, send their output to System.out. This is appropriate for a simple command-line tool; a more complex application might wish to redirect all script output to a log, or perhaps to swallow it unseen. Joe handles this via its outputHandler, a simple Consumer<String>.

var joe = new Joe();
joe.setOutputHandler(text -> log(text));

The script-level print() and println() functions all send their output through this handler. Further, Joe provides Java-level print() and println() functions that do the same; this allows the body of any native function to easily output whatever it likes.

private Object _square(Joe joe, Args args) {
    ...
    joe.println("In square()!");
}

Practical Usage

There are many ways to use Joe in an application or library. One can define a single instance of Joe and use it for running all scripts; or one can allow each scriptable component to have its own instance of Joe.


1

Not how to run Joe in an embedded hardware environment. Just to be clear.

Extending Joe

Joe can easily be extended with new functions, data types, and bindings to existing Java types. This section explains how the trick is done.

These sections will refer constantly to Joe's Java API. See the Joe Javadoc for details.

But first, a little philosophy.

Handwritten Bindings vs. Generated Bindings

It is possible to automatically generate script-level bindings for Java classes by using Java reflection or by scanning the Java source code, as Swig is often used to create dynamic language bindings for C APIs.

In my experience, an automatically generated binding falls far short of what can be done with a good handwritten binding. A handwritten binding:

  • Can take full advantage of the dynamic nature of the scripting language to provide a simpler, easier, more powerful API to the programmer, rather than simply mimicking the Java API.

  • Can trivially ignore those aspects of the Java API that need not or should not be exposed at the scripting level.

  • Can do a much better job of providing useful, detailed error messages to the user.

  • Need not expose dangerous Java APIs (e.g., network APIs) to clients of the binding.

It is therefore a non-goal of Joe to automatically provide bindings to existing classes via Java reflection or code generation. And it is very much a goal of Joe to make writing handwritten bindings as easily and pain-free as possible.

Joe and Java Data Types

Joe supports three kinds of data type:

Standard Types

Joe provides a number of standard data types, as described in the Types and Values section and the Joe Standard Library section. Values of these types are represented internally as normal Java values.

Joe TypeJava Type
Booleanjava.lang.Boolean
Errorcom.wjduquette.joe.JoeError (a RuntimeException)
Keywordcom.wjduquette.joe.Keyword
Listcom.wjduquette.joe.JoeValue (a List<Object>)
Numberjava.lang.Double
Stringjava.lang.String

When used in Java code, consequently, a Joe List is a Java List<Object>. A Joe string is a Java String. A Joe Number is a Java double. This greatly decreases the amount of work a binding needs to do when working with Joe values, compared to other embedded languages I have used.

A client binding should generally try to make the best use of these types.

Opaque Types

Every Java value, without exception1, is a valid Joe value, and can be assigned to Joe variables, passed to a Joe functions, and returned by Joe functions, whether Joe knows anything special about the value's type or not.

A Java type that Joe knows nothing special about is called an opaque type. But every such type is still a Java class and a subtype of java.lang.Object, and so "nothing special" is still quite a lot.
Given such a value, Joe can:

  • Determine the name of class it belongs to
  • Convert it to a string for output
  • Compare it for equality with other values
  • Use it as a key in a hash table
  • If it's a Collection<?>, iterate over its members

That might be all that's needed for many domain-specific use cases. When more is wanted, the client can register a proxy type for the type, thus turning the opaque type into a registered type.

Registered Types

A registered type is a Java type for which the client has registered a proxy type. The proxy type can give the Joe interpreter the ability to:

  • Create instances of the type
  • Call methods on values of the type
  • Access fields of values of the type
  • Customize the string representation for the type
  • Define static constants and methods for the type
  • Make the type iterable, so that its contents can be iterated over via the foreach statement.

Note: Most of the standard Joe types are nothing more than registered types that are registered automatically with every Joe interpreter. There's no magic; or, rather, the magic is fully available for use in client bindings.

1

Primitive types are represented as their boxed equivalents, e.g, double by Double.

Native Functions

A native function is a Java method that is exposed as a function at the script level. Every native function has the same Java signature:

Object _myFunction(Joe joe, Args args) { ... }
  • Every native function returns a Joe value, possibly null.
  • Every native function receives two arguments: joe, a reference to the Joe interpreter, and args, any arguments passed to the function.

It is the function's task to do the following:

  • Arity Checking: Ensure that it has received the correct number of arguments.
  • Argument Conversion: Ensure that those arguments have the desired Java data types, and throw meaningful errors if they do not.
  • Do the required computation—which often involves making a simple call to some Java API.
  • Return the Result: If any.

Joe provides the tools to make each of these steps simple and concise.

For example, a simple function to square a number would look like this:

Object _square(Joe joe, Args args) {
    args.exactArity(1, "square(x)");       // Arity check
    var x = joe.toDouble(args.next());     // Argument conversion
    return x*x;                            // Computation and return
}

Installing a Native Function

To install a native function, use the Joe::installGlobalFunction method:

var joe = new Joe();

joe.installGlobalFunction("square", this::_square);

Arity Checking

A function's arity is the number of arguments it takes. Joe provides four methods for checking that a native function has received the correct number of arguments. There are several cases:

Exact Arity

The Args::exactArity method checks that the function has received exactly a specific number of arguments, as shown in the _square() example above. It takes two arguments:

args.exactArity(1, "square(x)");
  • The number of arguments expected.
  • A string representing the function's signature.

If args doesn't contain exactly the correct number of arguments, exactArity() will use Args.arityFailure() to throw a JoeError with this message:

Wrong number of arguments, expected: square(x).

Minimum Arity and Arity Ranges

Similarly, the Args::minArity method checks that the args contains at least a minimum number of arguments. For example, Number.max() requires at least 1 argument but can take any number of arguments.

args.minArity(1, "Number.max(number,...)");

And the Args::arityRange method checks that the number of arguments false within a certain range. For example, the String type's substring() method takes 1 or 2 arguments:

args.arityRange(1, 2, "substring(beginIndex, [endIndex])");

More Complex Cases

In rare cases, it's simplest for the native function to access the Args's size directly, and throw an arityFailure explicitly:

if (args.size() > 7) {
    throw Args.arityFailure("myFunc(a, b, c, ...)");
}

Argument Conversion

Before passing an Object to a Java method, it's necessary to cast it or convert it to the required type, and to produce an appropriate error message if that cannot be done. Joe provides a family of argument conversion methods for this purpose, and client-specific converters are easily implemented.

For example,

var x = joe.toDouble(args.next());
  • args.next() pulls the next unprocessed argument from the args queue.
  • joe.toDouble() verifies that it's a Double and returns it as a double, or throws a JoeError.

The JoeError message will look like this:

Expected number, got: <actualType> '<actualValue>'.

See the Joe class's Javadoc for the family of converters, and the Joe class's source code for how they are implemented; and the many ProxyTypes in com.wjduquette.joe.types for examples of their use.

Converting Strings

There are two ways to convert an argument that is to be treated as a String.

  • joe.toString(arg) requires that the value is already a Java String.

  • joe.stringify(arg) will convert the argument, of whatever type, to its string representation. Proxy types can customize that representation.

Which of these you use will depend on the native function in question. To some extent it's a matter of taste.

Converting Booleans

In Joe, any value can be used in a Boolean expression. false and null are interpreted as false; any other value is interpreted as true. As a result, it's best to convert arguments meant to be used as Booleans with the Joe.isTruthy() function; this guarantees that the native function handles boolean values in the same way as Joe does at the script level:

var flag = Joe.isTruthy(arg);

Returning the Result

If the function is called for its side effects and has no real result, it must return null.

Any other value can be returned as is, but the following rules will make life easier:

Returning Integers: cast integer results to double. Joe understands doubles, but integers are an opaque type.

return (double)text.indexOf("abc");  // Convert integer index to double

Returning Lists: Most lists should be returned as ListValue values, which can be modified freely at the script level.

If Java code produces a list that need not or must not be modified, then it can be made read-only:

return joe.readonlyList(myList);

If the Java list is not a List<Object>, and needs to be modified in place at the script level, wrap it and specify the item type. (This is often necessary in JavaFX-related code):

return joe.wrapList(myList, MyItemType.class);

Joe will then ensure that only values assignable to MyItemType are added to the list. There are several variants of Joe::wrapList.

Other Collections: Joe supports maps and sets in much the same way as lists.

Registered Types

A registered type is a Java data type for which Joe has been provided a proxy type: an object that provides information about the type. Most of Joe's standard types, e.g., String, are implemented in just this way. This section explains how to define a ProxyType<V> and register it with Joe for use.

Defining A Proxy Type

A proxy type is a subclass of ProxyType<V>, where V is the proxied type. For example,

public class StringType extends ProxyType<String> {
    public StringType() {
        super("String");
    }
    ...
}

By convention, a Java proxy types have names ending with "Type", as in StringType. If the type can be subclassed by a scripted Joe class, then the proxy type's name should end in "Class", as in TextBuilderClass. If the type has no instances, but exists only as the owner of static methods and/or constants, it should end in "Singleton", as in the JoeSingleton class.

The constructor defines the various aspects of the proxy:

The Script-level Type Name

First, the proxy defines the script-level type name, which by convention should always begin with an uppercase letter. For example:

public class StringType extends ProxyType<String> {
    public StringType() {
        super("String");
        ...
    }
    ...
}

The script-level type name is often the same as the Java class name, but not always.

  • Joe's Number type is actually a Java Double; it's called Number because there's only one kind of number in Joe.
  • Joe's List type actually maps to two different List<Object> types, both under the umbrella of the JoeList interface. Calling it simply List is a kindness to the client.

When the proxy is registered Joe will create a global variable with the same name as the type, e.g., String.

The Proxied Types

Second, the proxy must explicitly identify the proxied type or types.

Usually a proxy will proxy the single type V, but if V is an interface or a base class, it might be desirable to explicitly identify the concrete classes. For example, a Joe String is just exactly a Java String.

public class StringType extends ProxyType<String> {
    public StringType() {
        super("String");
        proxies(String.class);
    }
    ...
}

But a Joe List could be a Java ListValue or ListWrapper, both of which implement the JoeList interface:

public class ListType extends ProxyType<JoeList> {
    public ListType() {
        super("List");
        proxies(ListValue.class);
        proxies(ListWrapper.class);
    }
    ...
}

Type Lookup

At runtime, Joe sees a value and looks up the value's proxy type in its type registry. This section explains how the lookup is done, as it can get complicated.

Joe keeps registered type information in the proxyTable, a map from Java Class objects to Java ProxyType<?> objects.

  • If value's Class is found in the proxyTable, the proxy is returned immediately. This is the most common case.

  • Next, Joe looks in the proxyTable for the value's superclass, and so on up the class hierarchy.

  • Next, Joe looks in the proxyTable for any interfaces implemented by the value's type, starting with the type's own Class and working its way up the class hierarchy.

  • Finally, if no proxy has been found then an OpaqueType proxy is created and registered for the value's Class.

Whatever proxy is found, it is cached back into the proxyTable for the value's concrete class so that it will be found immediately next time.

NOTE: when looking for registered interfaces, Joe only looks at the interfaces directly implemented by the value's class or its superclasses; it does not check any interfaces that those interfaces might extend. This is intentional, as expanding the search is too likely to lead to false positives and much unintentional comedy. Registered interfaces should be used with great care!

The Supertype

Sometimes it happens that both a type and its supertype are registered types; this is the case with Joe's Error and AssertError types. In such a case, the subtype's proxy can "inherit" the supertype's methods via the supertype() method:

public class AssertErrorProxy extends ProxyType<AssertError> {
  public AssertErrorProxy() {
    super("AssertError");
    proxies(AssertError.class);
    extendsProxy(ErrorProxy.TYPE);
  }
    ...
}

Stringification

When Joe converts a value to a string, it does so using a function called Joe::stringify. If the value belongs to a proxied type, Joe::stringify calls the proxy's stringify() method, which returns the result of the values toString() by default.

To change how a value is stringified at the script level, override ProxyType::stringify. For example, NumberType ensures that integer-valued numbers are displayed without a trailing .0:

@Override
public String stringify(Joe joe, Object value) {
    assert value instanceof Double;

    String text = ((Double)value).toString();
    if (text.endsWith(".0")) {
        text = text.substring(0, text.length() - 2);
    }
    return text;
}

Iterability

Joe's foreach statement, and its in and ni operators,
can iterate over or search the following kinds of collection values:

  • Any Java Collection<?>
  • Any value that implements the JoeIterable interface
  • Any proxied type whose ProxyType can produce a list of items for iteration.

To make your registered type iterable, provide an iterables supplier, a function that accepts a value of your type and returns a Collection<?>.

public class MyProxyType extends ProxyType<MyType> {
  public MyProxyType() {
    super("MyType");
        ...
        iterables(myValue -> myValue.getItems());
        ...
  }
    ...
}

Static Constants

A proxy may define any number of named constants, to be presented at the script level as properties of the type object. For example, NumberType defines several numeric constants. A constant is defined by its property name and the relevant Joe value.

public class NumberType extends ProxyType<Double> {
    public NumberType() {
        super("Number");
        ...
        constant("PI", Math.PI);
        ...
    }
    ...
}

In this case, the constant is accessible as Number.PI.

Static Methods

A proxy may also define any number of static methods, called as properties of the type object. For example, the String type defines the String.join() method, which joins the items in the list into a string with a given delimiter.

A static method is simply a native function defined as a property of a list object.

public class StringType extends ProxyType<String> {
    public StringType() {
        ...
        staticMethod("join", this::_join);
        ...
    }
    ...
    
    private Object _join(Joe joe, Args args) {
        args.exactArity(2, "join(delimiter, list)");
        ...
    }
}

Initializer

Most type proxies will provide an initializer function for creating values of the type. The initializer function is named after the type, returns a value of the type, and may take any desired arguments.

For example, the List type provides this initializer:

public class ListType extends ProxyType<JoeList> {
    public ListType() {
        super("List");
        ...
        initializer(this::_init);
        ...
    }

    private Object _init(Joe joe, Args args) {
        ... create a new list given the arguments ...
        return new ListValue(list);
    }

Static Types

A proxy type can be defined as a kind of library of static constants and methods. The NumberType is just such a proxy. Numbers have no methods in Joe, and they do not need an initializer as they can be typed directly into a script.

In this case, the type can be declared to be a static type. Among other things, this means that attempts to create an instance using the type's initializer will get a suitable error message.

public class NumberType extends ProxyType<Double> {
    public NumberType() {
        super("Number");
        staticType();
        proxies(Double.class);
        ...
    }
    ...
}

In the case of the Number type, the proxy still proxies an actual data type. This need not be the case. It is common to define a static type as a named library of functions with no related Java type. For example, consider a library to convert between Joe values and JSON strings. It might look like this:

public class JSONSingleton extends ProxyType<Void> {
    public JSONSingleton() {
        super("JSON");
        staticType();
        
        staticMethod("toJSON",   this::_toJSON);
        staticMethod("fromJSON", this::_fromJSON);
        ...
    }
    ...
}

Instance Methods

Most type proxies will define one or more instance methods for values of the type. For example, String and List provide a great many instance methods.

An instance method is quite similar to a native function, but an instance method is bound to a value of the proxied type, and so has a slightly different signature. For example, here is the implementation of the String type's length() method.

public class StringType extends ProxyType<String> {
    public StringType() {
        ...
        method("length", this::_length);
        ...
    }
    ...

    private Object _length(String value, Joe joe, Args args) {
        args.exactArity(0, "length()");
        return (double)value.length();
    }
}

Each instance method takes an initial argument that receives the bound value. The argument's type is type V, as defined in the proxy's extends clause. Otherwise, this is nothing more than a native function, and it is implemented in precisely the same way.

Installing a Proxy Type

Proxy types are installed using the Joe::installType method:

var joe = new Joe();

joe.installType(new MyType());

Installation creates the MyType object, and registers the type so that MyType's instance methods can be used with all values of MyType.

If a proxy type has no dynamic data, i.e., if it need not be configured with application-specific data in order to do its job, it is customary to create a single instance that can be reused with any number of instances of Joe. This is the case for all of the proxies in Joe's standard library. For example,

public class StringType extends ProxyType<String> {
    public static final StringType TYPE = new StringType();
    ...
}

This can then be installed as follows:

var joe = new Joe();

joe.installType(StringType.TYPE);

Joe Packages

A package is a collection of Joe functions and types intended to be added to a Joe interpreter as a unit. Joe's Standard Library is just such a package, as is the Test Tool API provided by joe test.

A package can be implemented in Java as a collection of native functions and registered types, or in Joe as a collection of Joe functions and classes, or as combination of the two.

Joe intentionally provides no means of installing a package at the script level; it is for the Java client to determine which packages it wishes to make available.1 Consequently, packages are usually defined at the Java level even when they are implemented primarily as Joe code.

Defining a Package

Defining a package is much like defining a registered type.

  • Subclass JoePackage
  • Give the package a name
  • Add content.

For example, the [Standard Library] package, joe, is defined like this:

public class StandardLibrary extends JoePackage {
    public static final StandardLibrary PACKAGE = new StandardLibrary();
    ...

    public StandardLibrary() {
        // Define the package name
        super("joe");

        // Add native functions
        globalFunction("catch", this::_catch);
        ...

        // Add native types
        type(BooleanProxy.TYPE);
        ...
        
        // Include script resources
        scriptResource(getClass(), "pkg.joe.joe");
    }
    ...
}

A global function is just a native function; when the package is installed, each defined global function will be installed automatically into the interpreter using Joe::installGlobalFunction. The function implementations are typically placed within the JoePackage subclass itself, as shown here.

The types are simply registered types; when the package is installed, each listed type will be installed automatically into the interpreter using Joe::installType. The actual proxies can be defined as nested classes in the JoePackage subclass, but are more typically defined in their own .java files.

Finally, a script resource is simply a Joe script found as a resource file in the project's jar file, usually adjacent to the package class that references it. At installation, the script resources will be loaded and executed after the global functions and native types.

Installing a Package

To install a package into an interpreter, use Joe::installPackage:

var joe = Joe();
joe.installPackage(new MyPackage());    // OR
joe.installPackage(MyPackage.PACKAGE);

Library Packages vs. Component Packages

Reusable libraries, such as Joe's standard library, are usually defined as JoePackage subclasses, as shown here.

Often, though, a particular component, e.g., joe test, will define a component-specific package of code, which is installed automatically by the component into the component's own instance of Joe. In this case the component might or might not define an explicit JoePackage subclass, as the JoePackage is primarily a convenient way for a client to install reusable bindings.

However, the component API should still be thought of as a package, and given a package name, as this is how distinct bindings are distinguished in the documentation produced by the joe doc tool.

1

Of course, a client could choose to provide a native function for importing its own packages....

Native Classes

TODO: Document how to create an extensible type in Java.

Native Records

Joe makes it easy to define bindings for native Java record types, and also to define bindings that make other Java types look like record types.

Consider the following Java record type:

record Thing(String id, Keyword color) {}

To create a binding, define ThingType as follows and install ThingType.TYPE into an instance of Joe in the usual way. The user can now create and use instances of Thing in their scripts.

public class ThingType extends ProxyType<Thing> {
    public static final ThingType TYPE = new ThingType();

    public ThingType() {
        super("Thing");

        proxies(Thing.class);
        
        initializer(this::_init);

        field("id",    Thing::id);
        field("color", Thing::color);
    }
    
    private Object _init(Joe joe, Args args) {
        args.exactArity(2, "Thing(id, color)");
        var id = joe.toString(args.next());
        var color = joe.toKeyword(args.next());
        return new Thing(id, color);
    }
}

The field() method declares that the type has a field with the given name, and specifies a lambda to retrieve it given an instance of the native type. As shown here the lambda is just a simple reference to the record's field, but more complex lambdas can be used freely.

Fields declared by field are read-only.

Constants, static methods, and instance methods can be added in the usual way.

joe version

The joe version tool outputs the current version number and the date and time at which it was built:

$ joe version
Joe 0.4.0 2024-11-15T15:46:03+0000
$

When run in development, the release MANIFEST.MF is not available. In this case, the output is as follows:

$ joe version
Joe (dev build)
$

joe run

The joe run tool is quite simple, and is intended primarily as a convenience to those learning the Joe language (and as a debugging aid for the Joe developer).

It takes a single argument, a Joe script to execute. It loads the script into a Joe interpreter, displaying any syntax errors; and if there are none executes the script and displays its output.

Here's a simple script.

// hello.joe
println("Hello, world!");

Execute it like this:

$ joe run hello.joe
Hello, world!
$

The joe.console API

The Joe interpreter used by joe run includes an optional Joe package, joe.console, that allows Joe scripts to access the command line arguments, read standard input and text files, and write text files. Follow the link for details.

Extending Joe

A vanilla Joe interpreter has no way of accessing the platform except by printing output, and even that can be redirected by the client application.

It is expected that projects using Joe will want to define an equivalent tool that includes the project-specific bindings. See Embedding Joe and Extending Joe for details.

joe repl

The joe repl tool is every bit as simple as joe run, and like it is intended primarily as a convenience to those learning the Joe language (and as a debugging aid for the Joe developer).

It takes no arguments, but simply invokes a simple interactive prompt for entering and execution Joe statements.

$ joe repl
> 1 + 1;
-> 2
> println("Hello, world!");
Hello, world!
>
$

To display the result of an expression, type in the expression followed by a semicolon, as shown. To execute the statement, do likewise.

To exit the REPL, press ^D or ^C.

The joe.console API

The Joe interpreter used by joe repl includes an optional Joe package, joe.console, that allows Joe scripts to access the command line arguments, read standard input and text files, and write text files. Follow the link for details.

Extending Joe

It is expected that projects using Joe will want to define an equivalent tool that includes the project-specific bindings, along with various other niceties. See Embedding Joe and Extending Joe for details.

joe test

The joe test tool is used to execute test scripts for Joe code and native APIs. This section explains how to write a test script, run tests, and interpret the results; and also how to extend Joe's test runner to include client-specific Joe packages.

Writing a Test Script

A test script is simply a Joe script containing tests, to be executed by the joe test tool. A test is a no-argument function whose name begins with the string "test":

function testSomething() {
    // Test Something
}

joe test loads the file and then executes each of the test functions one at a time, keeping track of the results. Note: the order in which the functions will be executed is not the same as the order in which they appear in the file. Take care to make each test independent of the other tests in the file.

Test Outcomes

For each test there are three possible outcomes:

ResultMeaning
SuccessThe test ran without error
FailureThe test threw an AssertError
ErrorThe test threw some other JoeError

Here's a successful test:

function testAddition() {
    var x = 2 + 2;
    assert x == 4;
}

And here's a failure:

function testAddition() {
    var x = 2 + 2;
    assert x == 5;
}

And here's an error; println() only takes a single argument

function testAddition() {
    var x = 2 + 2;
    println("x=", x);  // <- invalid syntax!
    assert x == 4;
}

Test Scaffolding

In addition to the test functions, a test script can also contain any number of other functions and classes, used as "scaffolding" by the tests.

However, individual test scripts are totally independent; they are executed in distinct interpreters, so that there is no possibility of cross-talk.

Test Checkers

The example above uses the assert statement to check the result of computations. This is a viable alternative; in addition, Joe's test runner defines a collection of functions and methods for testing results more simply and with better error messages. See the Joe Test Tool API for the details.

For example, the test above would more typically be written like this:

function testAddition() {
    var x = 2 + 2;
    check(x).eq(4);
}

Running Test Scripts

To run one or more test scripts, just pass their file names to joe test:

$ cd joe/tests
$ joe test *.joe

Successes: 118
Failures:  0
Error:     0
Total:     118

ALL TESTS PASS

By default, Joe will only display more information for failed tests:

$ joe test *.joe
testEndsWith                   in file type.joe.String.joe
  FAILED: Expected true, got: Boolean 'false'.

Successes: 117
Failures:  1
Error:     0
Total:     118

In particular, any output printed by tests is hidden. To see all test output, use the --verbose option (aka -v). The output will list each test script, each test name in the file, and any output related to each test.

$ joe test -v *.joe

Running: lang_class.joe

testCanSetFields               in file lang_class.joe
testStaticInitializer          in file lang_class.joe
testInit                       in file lang_class.joe
testStaticMethods              in file lang_class.joe
testCanCallMethods             in file lang_class.joe
testClassCapturesScope         in file lang_class.joe
...

Successes: 118
Failures:  0
Error:     0
Total:     118

ALL TESTS PASS

Extending the Test Runner

As with joe run and joe repl, Joe conducts all tests in a vanilla Joe interpreter; the only extensions are joe test's test API. A client project will usually wish to use the test runner for its own Joe bindings.

And as with joe run, Joe can't predict the context in which those bindings will need to be tested; it might be an entire scriptable application. Thus, Joe expects that client will provide their own test tool.

As of Joe 0.2.0, the simplest way to do this is to copy the test tool and modify the copy. The test tool is defined by the Java class com.wjduquette.joe.tools.test.TestTool. It is a short, straightforward file.

The relevant code is in the runTest() method:

// NEXT, configure the engine.
var joe = new Joe();
joe.installPackage(TestPackage.PACKAGE);

At this point in runTest, install any required native functions, types, or packages as described in Extending Joe.

We hope to make this simpler in a later release.

joe doc

The joe doc processor is used to extract JoeDoc documentation comments from .java and .joe source files, and from them generate Markdown API documentation.1

The API sections of this user's guide were generated by joe doc.

Documentation Set

A documentation set is the generated JoeDoc documentation for the packages in the current project. It will include documentation for the following types of entity:

  • Packages
    • Functions
    • Types (including Enums)
      • Constants
      • Static Methods
      • Initializers
      • Methods
    • Additional topics concerning a specific package or type
  • Mixins
    • A pseudo-type used for documentation content to be included in multiple types.

Unsurprisingly, this is the same structure you'll find in the Extending Joe section. And unsurprisingly, the generated documentation has this same structure.

Documentation Comments

A documentation comment is a block of comments beginning with the following marker comment: //** and continuing through contiguous comment lines until the next non-comment line:

// Not in a doc comment

//**
// @package my.package
//
// Information about `my.package` in Markdown format.
// ...

// Not in a doc comment

The comment prefix // must be the first non-whitespace text on each line.

joe doc extracts all doc comment lines from a source file, removes the comment prefixes, and then parses them to extract the documentation according to its structure.

Entity Tags

In the example just shown, the tag @package indicates that the following comment text concerns the named package. joe doc understands the following entity tags.

This is the same outline shown above, and purposely so. Each entity has a parent, and the parentage is indicated by the outline.

  • @package and @mixin entities belong to the documentation set.
  • Every @function and @type entity belongs to the @package that most nearly precedes it in the same file.
  • Every @constant, @static, @init, and @method entity belongs to the @type or @mixin that most nearly precedes it in the same file.
  • It's an error if the expected parent entity is missing.
  • joe doc checks carefully for duplicate names.
  • A @type can have at most a single @init (initializer) entity; and it has no name because it always has the same name as its type.

Packages are special; a single Joe package's code is often spread across a number of files. Thus, the same @package name can appear in multiple files, and the related entities will all be ascribed to the same package.

A single source file may (in principle) contain documentation for multiple mixins, packages and types. If joe doc sees a second @package, it immediately switches to that @package, and so for the others.

Example

Here is the beginning of the documentation in Joe's StandardLibrary.java file:

//**
// @package joe
// @title Joe Standard Library
// The `joe` package contains Joe's standard library.

... java code ...

//**
// @function catch
// @args callable
// @result CatchResult
// Executes the callable, which must not require any arguments.
// Returns a [[CatchResult]] indicating success or failure and providing
// the returned result or the error message respectively.
        
... java code ...

This file defines the package itself, and the functions in the package. Consequently, it begins with a comment describing the @package, and the goes on to document each @function in turn. Some things to note:

  • Each entity's documentation begins with its @entity tag.

  • This is followed by metadata tags: @title for the @package and @args and @result for the @function.

  • And these are followed by any number of lines of Markdown text. All such lines will be accumulated and ascribed to this entity until the next entity tag is seen.

Entity Types

This section describes each entity type and its metadata.

The @package Entity

The @package entity begins with @package <name>, and has the following optional metadata:

Metadata TagMeaning
@title stringGives the package a title.

The title string is used in the generated documentation.

The @package entity may have @function, @type, and @packageTopic children.

For example,

//**
// @package joe
// @title Joe Standard Library
// The `joe` package contains Joe's standard library.

The @packageTopic Entity

A package's normal Markdown content goes near the top of the generated package page, below the index but above the function entries. The @packageTopic entity is way to add titled Markdown content at the bottom of the package file, where it is out of the way.

The @packageTopic entity begins with @packageTopic <name>, and has the following metadata:

Metadata TagMeaning
@title stringGives the topic a title.

The title string is used as a header in the generated content, and in links to that content. In theory the @title tag is optional, but in practice one always wants to use it.

The @function Entity

The @function entity documents a global function, and begins with @function <name>. The name should be the name of a function defined in the current package.

The entity has the following optional metadata:

Metadata TagMeaning
@args <spec>Names the function's arguments.
@result <result>What the function returns.

The @args spec is typically a comma-delimited list of argument names, with ... used to indicate a variable length argument list and square brackets used to indicate optional arguments. Here are some examples:

  • x: The single argument x.
  • x, y: The two arguments x and y.
  • start, [end]: The two arguments start and end; end is optional.
  • x, ...: The argument x plus any number of additional arguments.
  • name, value, [name, value]...: One or more name,value pairs.

The @args tag can be omitted if the function takes no arguments, and can be repeated if the function has two or more distinct signatures.2

The @result's result is a single token, usually either the name of a Joe type or a variable name like value or this. It can be omitted if the function returns nothing (i.e., always returns null). If the result names a type, it will appear as a link to that type in the generated documentation.

The @type Entity

The @type entity documents a type, and so begins with @type <name>. The name should be the name of a type defined in the current package.

The entity has the following optional metadata:

Metadata TagMeaning
@extends <type>This type extends the named type.
@includeMixin <mixin>This type's documentation includes the named mixin.
  • <mixin> should be the name of a

The @extends <type> tag is used when this type extends (i.e., subclasses) another script-visible type. The <type> should be the name or qualified name of a referenced @type.

The @includesMixin <mixin> tag indicates that this type's documentation should include the documentation from a @mixin defined in this documentation set. The included documentation will be represented as part of the including type.

The @type entity may have @constant, @static, @init, @method, and @typeTopic children.

For example, in the standard library the AssertError type extends the Error type, both of which are visible at the script-level.

//**
// @type AssertError
// @extends Error
// ...

The @enum Entity

The @enum entity documents a native Java enumerated type whose binding is created using Joe's EnumProxy<E> type proxy class. An @enum entity is just like a @type entity, except that it automatically includes documentation for the enum's static methods, values() and valueOf, and for the enum's instance methods, name(), ordinal(), and toString().

A simple @enum doc comment will usually need to include only a description of the enum and the enum's @constant entities. If the specific enum defines other static or instance methods, they can be included as well.

The @typeTopic Entity

A type's normal Markdown content goes near the top of the generated type page, below the index but above the various constant and method entries. The @typeTopic entity is way to add titled Markdown content at the bottom of the type's file, where it is out of the way.

The @typeTopic entity begins with @typeTopic <name>, and has the following metadata:

Metadata TagMeaning
@title stringGives the topic a title.

The title string is used as a header in the generated content, and in links to that content. In theory the @title tag is optional, but in practice one always wants to use it.

As an example, the String type uses a @typeTopic to document its format string syntax.

The @constant Entity

The @constant entity documents a constant defined by a type, and so begins with @constant <name>. The name should be the name of a constant actually defined by the current type.

At present, the @constant entity has no metadata tags.

The @static Entity

The @static entity documents a static method defined by the current type, and begins with @static <name>. The name should be the name of a static method defined by the current type.

The @static entity has the same metadata as the @function entity.

The @init Entity

The @init entity documents the current type's initializer function. The @init tag doesn't include a name, as the initializer always has the same name as the type.

The @init entity accepts the @args metadata tag, just as any function or method does, but not the @result tag, as its result is always an instance of the current type.

The @method Entity

The @method entity documents a method defined by the current type, and begins with @method <name>. The name should be the name of a method defined by the current type.

The @method entity has the same metadata as the @function entity.

The @mixin Entity

The @mixin entity is like a pseudo-@type; it contains type content that would otherwise be duplicated in multiple @type entries. Mixin entities belong to the documentation set, not to any specific package, and may be included by any @type entity.

Every mixin begins with @mixin <name>; mixin names must be unique within the documentation set.

Mixins have no metadata tags, and may have Markdown content and @constant, @static, @method, and @typeTopic children.

When a type wants to include a mixin, it uses the @includesMixin <name> metadata tag.

  • The mixin's Markdown content is inserted into the type's Markdown content at the location of the @includesMixin tag
  • The mixin's children are then added to the type.

A mixin is like a template; the string <type> may be included anywhere in the content of the mixin itself and its children, and will be replaced on inclusion by the including type's name.

For example, the mixin might define:

//**
// @mixin MyMixin
// 
// The `<type>` type is able to...
//
// @method myMethod
// @result <type>
// This method...

Then, another type can include it like this:

//**
// @type MyType
// The `MyType` type represents...
//
// @includeMixin MyMixin
//
// The MyMixin Markdown content is included above, replacing the include tag.

The Markdown content for each entity can of course contain normal Markdown links. In addition, though, JoeDoc supports abbreviated links to any entity in the documentation set. For example, if [[String]] is found in Markdown content for the joe package, it will be replaced by a link to the file documenting the String type.

Qualified Type Names

Every @type entity is defined within a package, and so has two names: its bare or unqualified name, e.g., String, and its qualified name, joe.String.

Entity Mnemonics

Every entity can be identified in a JoeDoc link by its mnemonic, which may be qualified or unqualified.

  • An entity can be linked to by its unqualified mnemonic from any Markdown content in the same package.
  • An entity can be linked to by its qualified mnemonic from any Markdown content in the entire documentation set.
EntityQualifiedUnqualified
@package<pkg><pkg>
@packageTopic<pkg>#topic.<name><pkg>#topic.<name>
@function<pkg>#function.<name>function.<name>
@type<pkg>.<type><type>
@typeTopic<pkg>.<type>#topic.<name><type>#topic.<name>
@constant<pkg>.<type>#constant.<name><type>#constant.<name>
@static<pkg>.<type>#static.<name><type>#static.<name>
@init<pkg>.<type>#init<type>#init
@method<pkg>.<type>#method.<name><type>#method.<name>

Thus,

  • [[joe.String#method.length]] links to the String type's length method, from anywhere in the documentation set.
  • [[String#method.length]] links to the String type's length method, from anywhere in the joe package's own documentation.

Running joe doc

To run joe doc, cd to the folder containing the joe doc configuration file, doc_config.joe, and then run joe doc. In Joe's own case, it looks like this:

$ cd joe/mdbook
$ ls doc_config.joe
doc_config.joe
$ joe doc
...
$

joe doc will report any parsing errors, duplicate entities, or unrecognized mnemonics in JoeDoc links.

Configuring joe doc

The joe doc configuration file, doc_config.joe, is itself a Joe script, using the API defined in the joe.doc package.

Joe's own doc_config.file looks like this (at time of writing):

DocConfig
    .inputFolder("../lib/src/main/java/com/wjduquette/joe")
    .inputFolder("../lib/src/main/resources/com/wjduquette/joe")
    .outputFolder("src/library");

All file paths shown are relative to the location of the doc_config.joe file.

Each inputFolder() method names a folder that contains .java and/or .joe files to scan for doc comments. joe doc will scan for such files in the named folder, recursing down into subfolders.

There is also an inputFile() method that names a specific file to scan.

The outputFolder() method names the folder to receive the generated documentation files.

Generated Files

When run, joe doc produces the following files, all within the configured output directory.

  • index.md: A detailed index of the entire documentation set, e.g., the Joe Library API Index.
  • pkg.<pkg>.md: The package file for the given package., e.g., pkg.joe.md
  • type.<pkg>.<type>.md: The type file for the given type, e.g., type.joe.String.md

The link IDs for the functions in a package file, and for the methods and other entities in a type file, are exactly the same as in the entity mnemonics. Thus, to link to the length method of the String type from outside the documentation set, you'd use the partial URL <folder>/type.joe.String.md#method.length.

1

This User's Guide is produced using mdBook, and so joe doc produces output in a form compatible with mdBook. It should be usable with any other static site generator that works with Markdown files.

2

Joe does not allow for overloaded function and method names, but a single function or method can simulate overloading through the use of a variable length argument list. This technique is common in native functions and methods.

joe nero

The joe nero tool executes standalone Nero programs from the command line.

Given simple.nero:

Parent(#anne, #bert);
Parent(#bert, #clark);
Ancestor(x, y) :- Parent(x, y);
Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
$ joe nero simple.nero
New Facts:
Ancestor(#anne, #bert)
Ancestor(#anne, #clark)
Ancestor(#bert, #clark)

To see the axioms as well as the facts inferred by the rules, pass --all:

$ joe nero --all simple.nero
All Facts:
Ancestor(#anne, #bert)
Ancestor(#anne, #clark)
Ancestor(#bert, #clark)
Parent(#anne, #bert)
Parent(#bert, #clark)

The --debug flag outputs a Nero execution trace (the precise output is subject to change without notice):

$ joe nero --debug simple.nero 
Rule Strata: [[Ancestor]]
Iteration 0.1:
  Rule: Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
  Rule: Ancestor(x, y) :- Parent(x, y);
    Fact: Ancestor(#bert, #clark)
    Fact: Ancestor(#anne, #bert)
Iteration 0.2:
  Rule: Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
    Fact: Ancestor(#anne, #clark)
  Rule: Ancestor(x, y) :- Parent(x, y);
Iteration 0.3:
  Rule: Ancestor(x, y) :- Parent(x, z), Ancestor(z, y);
  Rule: Ancestor(x, y) :- Parent(x, y);
New Facts:
Ancestor(#anne, #bert)
Ancestor(#anne, #clark)
Ancestor(#bert, #clark)

See joe help nero for the complete command line syntax.

Library API Index


The following is a complete index of the packages, functions, types, methods, and constants include in Joe's aPackage.

Joe Standard Library (joe)


The joe package contains Joe's standard library.

Topics

Functions

Types

Functions


catch()


catch(callable) → CatchResult

Executes the callable, which must not require any arguments. Returns a CatchResult indicating success or failure and providing the returned result or the error message respectively.

print()


print(text)

Prints its text to standard output (which might be redirected by the application).

printf()


printf(fmt, [values...])

Formats its arguments given the fmt string, and prints the result to standard output (which might be redirected by the application).

See String Formatting for the format string syntax.

println()


println([text])

Prints its text followed by a line separator to standard output (which might be redirected by the application).

Why "Joe"?


My friend Joe's a straight shooter, and never misses his mark.

AssertError type (joe)


Extends: Error

The AssertError type represents an exception thrown during the execution of a Joe script by the assert statement, or created by the joe.test package to represent a test value. A script can catch errors thrown during execution using the catch() function.

Initializer

Error Methods

AssertError Initializer


AssertError(message, [trace, ...]) → AssertError

Creates an AssertError with the given message and informational trace messages.

Boolean type (joe)


The Boolean type has the expected values true and false.

Static Methods

Static Methods


Boolean.valueOf()


Boolean.valueOf(string) → Boolean

Returns true if string equals the string "true", ignoring case, and false for all other values, including null.

Note: this is consistent with the Java behavior.

CatchResult type (joe)


The result type of the standard catch() function. The record has two fields, result and error.

Static Methods

Methods

Static Methods


CatchResult.error()


CatchResult.error(error) → CatchResult

Creates a new CatchResult indicating a failure. The error is the specific error, either a String or an Error. The CatchResult's error field will be set to the error, and its result field will be null.

CatchResult.ok()


CatchResult.ok(result) → CatchResult

Creates a new CatchResult indicating a successful result. The result is the result value; the error field will be null.

Methods


catchResult.isError()


catchResult.isError() → Boolean

Returns true if the CatchResult represents an error and false if it represents a success.

catchResult.isOK()


catchResult.isOK() → Boolean

Returns true if the CatchResult represents a success and false if it represents an error.

Error type (joe)


Extended By: AssertError

The Error type represents an exception thrown during the execution of a Joe script. A script can catch errors thrown during execution using the catch() function.

Initializer

Methods

Error Initializer


Error(message, [trace, ...]) → Error

Creates an Error with the given message and optional information trace messages.

Methods


error.addInfo()


error.addInfo(message) → this

Adds an information message to the list of traces.

error.javaStackTrace()


error.javaStackTrace() → String

Returns the complete error, including the initial error messages and all stack frames.

error.message()


error.message() → text

Gets the actual error message

error.stackTrace()


error.stackTrace() → String

Returns the complete error, including the initial error messages and all stack frames.

error.traces()


error.traces() → List

Returns the list of trace strings. Clients may add to the list using addInfo(). and rethrow the error.

error.type()


error.type() → name

Gets the name of the concrete error type.

Fact type (joe)


An ad hoc type for Nero facts, consisting of a relation name and ordered or named fields. A Nero ruleset produces Fact values by default, and also accepts Facts as input. Many Joe values can be converted to Facts.

Static Methods

Initializer

Methods

Fact Fields and Field Names

A Fact's field values are accessible by the fields() method if the Fact isOrdered(), and as a map of field names and values via the fieldMap() method.

In addition, a fact's fields can be access as normal Joe object fields:

// Simple ordered fact; field names are `f1` and `f2`.
var fact = Fact("Thing", #car, #red);

// Prints "This Thing is #red"
println("This " + fact.relation() + " is " + fact.f2);

// Simple unordered fact; field names are as given.
var fact2 = Fact.ofMap("Thing", {"id": #car, "color": #red});

// Prints "This Thing is #red"
println("This " + fact.relation() + " is " + fact.color);

Static Methods


Fact.of()


Fact.of(relation, fields)

Creates a new ordered Fact given the relation and a list of field values. Its fields will be named f0, f1, etc.

The Fact will be an instance of the Java ListFact class.

Fact.ofMap()


Fact.ofMap(relation, fieldMap)

Creates a new unordered Fact given the relation and the field map. Its fields will have the names given as keys in the map.

The Fact will be an instance of the Java MapFact class.

Fact.ofPairs()


Fact.ofPairs(relation, pairs)

Creates a new ordered Fact given a flat list of field name/value pairs. Its fields will have the names given in the list.

The Fact will be an instance of the Java RecordFact class.

Fact Initializer


Fact(relation, field, ...) → Fact

Creates a new Fact given the relation and one or more the field values. Its fields will be named f0, f1, etc.

The Fact will be an instance of the Java ListFact class.

Methods


fact.fieldMap()


fact.fieldMap() → Map

Returns a read-only map of the field values.

fact.fields()


fact.fields() → List

Returns a read-only list of the field values, if the fact isOrdered().

fact.isOrdered()


fact.isOrdered() → Boolean

Returns true if the fact has ordered fields, and false otherwise.

fact.relation()


fact.relation() → String

Returns the Fact's relation name.

fact.toString()


fact.toString() → String

Returns the value's string representation.

Function type (joe)


Function is the type of all Joe callables.

Joe type (joe)


The Joe singleton collects a variety of useful functions related to the interpreter itself and to the installed Joe types.

Static Methods

Static Methods


Joe.compare()


Joe.compare(a, b) → Number

Given two strings or two numbers a and b, returns -1, 0, or 1 as a < b, a == b, or a > b. This function is useful when sorting collections.

Joe.currentTimeMillis()


Joe.currentTimeMillis() → Number

Returns Java's System.currentTimeMillis(), the current time in milliseconds since the epoch.

Joe.getFieldNames()


Joe.getFieldNames(value) → List

Returns a read-only list of the names of the fields defined on the value.

If the value has no fields, this method returns the empty list.

If the value is a Joe type, this method returns the names of the type's static fields.

Note: There is no way to know the names of an instance's fields just by looking at its type, generally speaking. Distinct instances of the same Joe class might have different fields.

Joe.isFact()


Joe.isFact(value) → Boolean

Returns true if the value can be used as a Nero Fact, and false otherwise.

Joe.isOpaque()


Joe.isOpaque(value) → Boolean

Returns true if the value is of an opaque type, and false otherwise. An opaque type is a Java type with no defined Joe binding.

Joe.isType()


Joe.isType(value) → Boolean

Returns true if the value represents a Joe type, e.g., the String type or a Joe class, and false otherwise.

Joe.stringify()


Joe.stringify(value) → String

Converts its value to a string for output. This function is functionally equivalent to String(), or to value.toString() (if the type defines a toString() method).

It is rare to need to call this function directly, but it is available if needed.

Joe.supertypeOf()


Joe.supertypeOf(type) → type

Returns the supertype of the given type, or null if the type has no supertype.

Joe.toFact()


Joe.toFact(value) → Fact

Returns the value as a Nero Fact. Throws an Error if !isFact(value).

Joe.typeOf()


Joe.typeOf(value) → type

Returns the Joe type of the given value.

Keyword type (joe)


A Keyword is a symbolic value, and one of the basic Joe types. A keyword is an identifier with a leading hash symbol, e.g., #flag. They are commonly used in Joe code in place of enumerations, especially to identify options in variable length argument lists.

Initializer

Methods

Keyword Initializer


Keyword(name) → Keyword

Creates a new keyword given its name, without or without the leading #.

Methods


keyword.name()


keyword.name() → String

Gets the keyword's name, omitting the leading #.

keyword.toString()


keyword.toString() → String

Gets the keyword's string representation, including the leading #.

List type (joe)


A Joe List is a Java List, roughly equivalent to a Java ArrayList. Lists created using the List() initializer can contain any kind of Joe value; the list need not be homogeneous. Lists received from Java code might be read-only or require a specific item type.

Static Methods

Initializer

Methods

Static Methods


List.of()


List.of(items...) → List

Creates a list containing the arguments.

List Initializer


List() → List
List(other) → List
List(size, [initValue]) → List

Creates a new list as a copy of the other list, or as an empty list of the given size. The list elements will be filled with the initValue, or with null if initValue is omitted.

Methods


list.add()


list.add([index], item) → this

Adds the item to the list at the given index, which defaults to the end of the list.

list.addAll()


list.addAll([index], collection) → this

Adds all items in the collection to the list at the given index, which defaults to the end of the list.

list.clear()


list.clear() → this

Removes all items from the list.

list.contains()


list.contains(value) → Boolean

Returns true if the list contains the value, and false otherwise.

list.containsAll()


list.containsAll(collection) → Boolean

Returns true if the list contains all the values in the collection, and false otherwise.

list.copy()


list.copy() → List

Returns a shallow copy of the list.

list.filter()


list.filter(predicate) → List

Returns a list containing the elements for which the filter predicate is true.

list.get()


list.get(index) → value

Returns the value at the given index.

list.getFirst()


list.getFirst() → value

Returns the first value in the list. It's an error if the list is empty.

list.getLast()


list.getLast() → value

Returns the last value in the list. It's an error if the list is empty.

list.indexOf()


list.indexOf(value) → Number

Returns the index of the first occurrence of the value in the list, or -1 if not found.

list.isEmpty()


list.isEmpty() → Boolean

Returns true if the list is empty, and false otherwise.

list.lastIndexOf()


list.lastIndexOf(value) → Number

Returns the index of the last occurrence of the value in the list, or -1 if not found.

list.map()


list.map(func) → List

Returns a list containing the items that result from applying function func to each item in this list.

list.peekFirst()


list.peekFirst() → value

Returns the first value in the list, or null if the list is empty.

list.peekLast()


list.peekLast() → value

Returns the last value in the list, or null if the list is empty.

list.remove()


list.remove(value) → Boolean

Removes the value from the list if it's present. Returns true if the item was removed, and false if it was not present

list.removeAll()


list.removeAll(collection) → Boolean

Removes all items in the list that are found in the collection. Returns true if any items were removed, false otherwise.

list.removeAt()


list.removeAt(index) → value

Removes and returns the value at the given index.

list.removeFirst()


list.removeFirst() → value

Removes and returns the first value in the list. It's an error if the list is empty.

list.removeLast()


list.removeLast() → value

Removes and returns the last value in the list. It's an error if the list is empty.

list.reverse()


list.reverse() → List

Returns a reversed copy of the list.

list.set()


list.set(index, newValue) → oldValue

Puts the value at the given index, returning the oldValue at that index.

list.size()


list.size() → Number

Returns the number of items in the list.

list.sorted()


list.sorted([comparator]) → List

Returns a list, sorted in ascending order. If no comparator is provided, the list must be a list of strings or a list of numbers. If a comparator is given, it must be a function that takes two arguments and returns -1, 0, 1, like the standard Joe.compare() function.

To sort in descending order, provide a comparator that reverses the comparison.

var list = List(1,2,3,4,5);
var descending = list.sorted(\a,b -> -compare(a,b));

list.sublist()


list.sublist(start, [end]) → List

Returns the sublist of this list that starts at start and ends before end, which defaults to the end of the list.

list.toString()


list.toString() → String

Returns the string representation of this list.

Map type (joe)


A Joe Map is a Java Map, roughly equivalent to a Java HashMap. Maps created using the Map() initializer can contain any kind of Joe keys and values; the map need not be homogeneous. Maps received from Java code might be read-only or require a specific key/value types.

Static Methods

Initializer

Methods

Static Methods


Map.of()


Map.of(values...)

Creates a Map of the argument values, which must be a flat list of key/value pairs.

Map Initializer


Map([other]) → Map

Creates a new Map, optionally initializing it with the entries from the other map.

Methods


map.clear()


map.clear() → this

Empties the map.

map.containsKey()


map.containsKey(key) → Boolean

Returns true if the map contains the key, and false otherwise.

map.containsValue()


map.containsValue(value) → Boolean

Returns true if the map has at least one key with the given value, and false otherwise.

map.copy()


map.copy() → Map

Returns a shallow copy of this map.

map.get()


map.get(key) → value

Gets the key's value, or null if the key is not found.

map.getOrDefault()


map.getOrDefault(key, defaultValue) → value

Gets the key's value, or the defaultValue if the key is not found.

map.isEmpty()


map.isEmpty() → Boolean

Returns true if the map is empty, and false otherwise.

map.keySet()


map.keySet() → Set

Returns a set of the keys in the map.

map.put()


map.put(key, value) → value

Adds the *key/value pair to the map, returning the replaced value.

map.putAll()


map.putAll(map) → this

Adds the content of the map to this map.

map.remove()


map.remove(key) → value

Removes and returns the key's value, or null if the key wasn't found.

map.size()


map.size() → Number

Returns the number of key/value pairs in the map.

map.toString()


map.toString() → String

Returns the map's string representation.

map.values()


map.values() → List

Returns a list of the values in the map.

Number type (joe)


The Number type is a static type that collects together a number of useful constants and numeric methods. Most of the methods gathered here are delegated directly to Java's java.lang.Math class; numeric details are to be found there.

Constants

Static Methods

Initializer

Constants


Number.E


The double-precision value that is closer than any other to e, the base of the natural logarithms.

Number.MAX_INT


The maximum value of a Java integer.

Number.MAX_VALUE


The maximum value of a Java double.

Number.MIN_INT


The minimum (most negative) value of a Java integer.

Number.MIN_VALUE


The minimum (most negative) value of a Java double.

Number.NAN


The double-precision value signifying "not a number".

Number.NEGATIVE_INFINITY


The double-precision value signifying negative infinity.

Number.PI


The double-precision value that is closer than any other to 𝛑, the ratio of the circumference of a circle to its diameter.

Number.POSITIVE_INFINITY


The double-precision value signifying positive infinity.

Number.TAU


The double-precision value that is closer than any other to 𝛕, the ratio of the circumference of a circle to its radius.

Static Methods


Number.abs()


Number.abs(num) → Number

Returns the absolute value of the given number.

Number.acos()


Number.acos(num) → Number

Returns the arc cosine of the number

Number.asin()


Number.asin(num) → Number

Returns the arc sine of the number.

Number.atan()


Number.atan(num) → Number

Returns the arc tangent of the number.

Number.atan2()


Number.atan2(x, y) → Number

Returns the angle theta from the conversion of rectangular coordinates (x, y) to polar coordinates (r, theta).

Number.ceil()


Number.ceil(num) → Number

Returns the smallest integer number that is greater than or equal to num.

Number.clamp()


Number.clamp(num, min, max) → Number

Clamps num to fit between min and max.

Number.cos()


Number.cos(a) → Number

Returns the cosine of angle a.

Number.exp()


Number.exp(num) → Number

Returns Number.E raised the num power.

Number.floor()


Number.floor(num) → Number

Returns the largest integer number that is less than or equal to num.

Number.hypot()


Number.hypot(x, y) → Number

Returns the length of the hypotenuse of a right triangle with legs of length x and y.

Number.log()


Number.log(num) → Number

Returns the natural logarithm of the number.

Number.log10()


Number.log10(num) → Number

Returns the base-10 logarithm of the number.

Number.max()


Number.max(num...) → Number

Given one or more numbers, returns the maximum value. The numbers may be passed as individual arguments or as a single List.

Number.min()


Number.min(num...) → Number

Given one or more numbers, returns the minimum value. The numbers may be passed as individual arguments or as a single List.

Number.mod()


Number.mod(a, b) → Number

Returns the integer modulus a % b as computed in Java. Both terms are converted to integers.

Number.pow()


Number.pow(a, b) → Number

Returns a raised to the b power.

Number.random()


Number.random() → Number

Returns a random number in the range 0.0 <= x < 1.0.

Number.round()


Number.round(num) → Number

Returns the closest integer number to num, rounding ties toward positive infinity.

Number.sin()


Number.sin(a) → Number

Returns the sine of the angle.

Number.sqrt()


Number.sqrt(num) → Number

Returns the square root of the number.

Number.tan()


Number.tan(a) → Number

Returns the tangent of the angle.

Number.toDegrees()


Number.toDegrees(radians) → Number

Converts an angle in radians to an angle in degrees.

Number.toRadians()


Number.toRadians(degrees) → Number

Converts an angle in degrees to an angle in radians.

Number Initializer


Number(string) → Number

Converts a numeric string to a number. Supports Joe's numeric literal syntax, including hexadecimals.

Opaque type (joe)


Opaque is the "type" of all opaque Java values: the fallback when Joe has no other type information for a value. Thus, this page describes the interface of the type returned by

Methods

var type = Joe.typeOf(someOpaqueValue);

Note: there is no Opaque type defined in the global scope; Joe.typeOf() will return a distinct opaque type for each distinct Java class with no explicit Joe binding.

Methods


opaque.javaName()


opaque.javaName() → String

Returns the full name of the Java class to which the opaque value belongs. In Java terms:

return value.getClass().getName();

opaque.name()


opaque.name() → String

Returns the simple name of the Java class to which the opaque value belongs. In Java terms:

return value.getClass().getSimpleName();

opaque.toString()


opaque.toString() → String

Returns the same string as the javaName() method.

RuleSet type (joe)


A Nero rule set, as created by the ruleset expression. A rule set contains Nero rules and optionally some number of base facts. A rule set can infer new facts given its rules, base facts, and any input facts provided by the script.

Methods

Methods


ruleSet.infer()


ruleSet.infer([inputs]) → Set

Returns a set of known facts, both input and inferred, given the rule set and any provided inputs.

NOTE: By default, a rule set cannot infer facts of the same types as the given inputs: infer(inputs) throws an error if any input fact's type name is the same as a relation used in one of the rule set's rule heads or axioms.

However, an export declaration can achieve the same effect. See the Nero tutorial for details.

ruleSet.isDebug()


ruleSet.isDebug() → Boolean

Returns the rule set's debug flag.

ruleSet.isStratified()


ruleSet.isStratified() → Boolean

Returns whether the rule set is stratified or not.

ruleSet.setDebug()


ruleSet.setDebug(flag)

Sets the rule set's debug flag. If enabled, infer() will output a detailed execution trace.

ruleSet.toString()


ruleSet.toString() → String

Returns the value's string representation.

Set type (joe)


A Joe Set is a Java Set, roughly equivalent to a Java HashSet. Sets created using the Set() initializer can contain any kind of Joe value; the set need not be homogeneous. Sets received from Java code might be read-only or require a specific value type.

Initializer

Methods

Set Initializer


Set(values...) → Set

Creates a Set of the argument values, which must be a flat list of key/value pairs.

Methods


set.add()


set.add(value) → Boolean

Adds the value to the set, returning true if it wasn't already present.

set.addAll()


set.addAll(collection) → Boolean

Adds the content of the collection to this set.

set.clear()


set.clear() → this

Empties the set.

set.contains()


set.contains(value) → Boolean

Returns true if the set contains the value, and false otherwise.

set.copy()


set.copy() → Set

Returns a shallow copy of this set.

set.filter()


set.filter(predicate) → Set

Returns a list containing the elements for which the filter predicate is true.

set.isEmpty()


set.isEmpty() → Boolean

Returns true if the set is empty, and false otherwise.

set.map()


set.map(func) → Set

Returns a set containing the items that result from applying function func to each item in this set.

set.remove()


set.remove(value) → Boolean

Removes the value, return true if it was present and false otherwise.

set.removeAll()


set.removeAll(collection) → Boolean

Removes all values in the collection from the set, returning true if the set changed and false otherwise.

set.size()


set.size() → Number

Returns the number of values in the set.

set.sorted()


set.sorted([comparator]) → List

Returns a list of the set's items, sorted in ascending order. If no comparator is provided, then this set must be a set of strings or numbers. If a comparator is given, it must be a function that takes two arguments and returns -1, 0, 1, like the standard Joe.compare() function.

To sort in descending order, provide a comparator that reverses the comparison.

set.toString()


set.toString() → String

Returns the set's string representation.

String type (joe)


A String is just a Java String, as accessed from a Joe script.

Topics

Static Methods

Initializer

Methods

Note: Joe provides a number of methods that Java does not.

Static Methods


String.format()


String.format(fmt, [values...]) → String

Formats the values into a string given the format string. See String Formatting, below, for the format string syntax.

String.join()


String.join(delimiter, list) → String

Given a delimiter and a list, joins the string representations of the list items into a single string, delimited by the given delimiter.

String Initializer


String(value) → String

Converts the value into its string representation.

Methods


string.charAt()


string.charAt(index) → String

Returns the character at the index as a string.

string.contains()


string.contains(target) → Boolean

Returns true if this contains the target, and false otherwise.

string.endsWith()


string.endsWith(suffix) → Boolean

Returns true if this string ends with the suffix, and false otherwise.

string.equalsIgnoreCase()


string.equalsIgnoreCase(other) → Boolean

Returns true if this string is equal to the string representation of the other value, ignoring case, and false otherwise.

string.indent()


string.indent(n) → String

Indents or outdents the string by n characters.

Note: Java's String::indent returns the result with a trailing newline; this is easier to add than to remove, and is often unwanted, so Joe trims it.

string.indexOf()


string.indexOf(target, [beginIndex], [endIndex]) → Number

Returns the index of the first occurrence of the target string in this string, or -1 if the target is not found. The search starts at beginIndex, which defaults to 0, and ends at endIndex, which defaults to the end of the string.

string.isBlank()


string.isBlank() → Boolean

Returns true if this string is empty or contains only whitespace, and false otherwise.

string.isEmpty()


string.isEmpty() → Boolean

Returns true if this string is the empty string, and false otherwise.

string.lastIndexOf()


string.lastIndexOf(target, [fromIndex]) → Number

Returns the index of the last occurrence of the target string in this string, or -1 if the target is not found. The search starts at fromIndex, which defaults to 0, and proceeds towards the start of the string.

string.length()


string.length() → Double

Gets the string's length.

string.lines()


string.lines() → List

Returns a list consisting of the lines of text in the string.

string.matches()


string.matches(pattern) → Boolean

Returns true if this string matches the regex pattern, and false otherwise.

string.repeat()


string.repeat(count) → String

Creates a string containing this string repeated count times.

string.replace()


string.replace(target, replacement) → String

Returns the string created by replacing every occurrence of the target string in this string with the replacement string.

string.replaceAll()


string.replaceAll(regex, replacement) → String

Returns the string created by replacing each substring of this string that matches the regex with the replacement string.

string.replaceFirst()


string.replaceFirst(regex, replacement) → String

Returns the string created by replacing the first substring of this string that matches the regex with the replacement string.

string.split()


string.split(delimiter) → List

Returns a list of the tokens formed by splitting the string on each of the substrings that match the delimiter regular expression pattern. The delimiter substrings are not included in the list.

string.splitWithDelimiters()


string.splitWithDelimiters(delimiter) → List

Returns a list of the tokens formed by splitting the string on each of the substrings that match the delimiter regular expression pattern. The delimiter substrings are included as tokens in the list.

string.startsWith()


string.startsWith(prefix) → Boolean

Returns true if this string starts with the prefix, and false otherwise.

string.strip()


string.strip() → String

Returns this string, stripped of all leading and trailing whitespace.

string.stripIndent()


string.stripIndent() → String

Strips the indent from each line of the string, preserving relative indentation.

string.stripLeading()


string.stripLeading() → String

Returns this string, stripped of all leading whitespace.

string.stripTrailing()


string.stripTrailing() → String

Returns this string, stripped of all trailing whitespace.

string.substring()


string.substring(beginIndex, [endIndex]) → String

Returns the substring of this string that starts at the beginIndex and ends at the endIndex; endIndex defaults to the end of the string.

string.toLowerCase()


string.toLowerCase() → String

Returns this string converted to lowercase.

string.toString()


string.toString() → String

Returns this string.

string.toUpperCase()


string.toUpperCase() → String

Returns this string converted to uppercase.

String Formatting


The Joe String.format() method formats strings similarly to the Java method of the same name, supporting a subset of Java's format string syntax.

This section describes the supported subset.

As with Java, the basic syntax is:

%[flags][width][.precision][conversion]

Conversions

The conversion codes are as follows:

ConversionInput TypeDescription
b, BAnytrue if truthy, false if falsey
h, HAnyThe value's hash code as a hex string.
s, SAnyThe value, stringified.
dNumberConverted to integer and formatted.
x, XNumberConverted to integer, formatted as hex
e, ENumberDecimal, scientific notation
fNumberDecimal
g, GNumberDecimal or scientific notation, as appropriate.
%n/aA literal percent sign
nn/aThe platform-specific line separator
  • Note: the uppercase variant of the conversion is the same as the lowercase variant, but converts the output to uppercase.

Precision

  • For decimal conversions, the precision is the number of digits after the decimal place.

  • For the conversions with input type "Any" in the above table, the natural length of the output might be longer than the desired width. In this case, the precision field specifies the maximum width of the output.

  • The precision field cannot be used with the d, x, and X conversions.

Flags

The supported flags are as follows, and apply only to the specified argument type.

FlagInput TypeDescription
-AnyResult is left-justified.
+NumberResult always includes a sign
spaceNumberLeading space for positive values
0NumberPadded with leading zeros
,NumberUse local-specific grouping separators
(NumberEnclose negative numbers in parentheses

What's Not Supported

Joe's String.format() method does not support the following:

  • Argument indices, using $ syntax.
  • Date/time formatting.

I don't use argument indices with Java's String.format(), as I find them to be fragile; and I've always found it weird to combine simple type formatting and complex date/time formatting in what's meant to be a type-safe clone of Java's printf() syntax. I'd much prefer to provide a distinct API for using Java's java.time package.

TextBuilder type (joe)


TextBuilder is a native type similar to a Java StringBuilder; it's used for building up text strings a little at a time.

Initializer

Methods

Joe classes can extend the TextBuilder type.

TextBuilder Initializer


TextBuilder() → TextBuilder

Creates an empty TextBuilder.

Methods


textBuilder.append()


textBuilder.append(value) → this

Adds the value to the buffer.

textBuilder.clear()


textBuilder.clear() → this

Clears the buffer.

textBuilder.print()


textBuilder.print(value) → this

Adds the value to the buffer.

textBuilder.printf()


textBuilder.printf(fmt, [values...])

Formats its arguments given the fmt string, and appends the result to the buffer. See String Formatting for the format string syntax.

textBuilder.println()


textBuilder.println(value) → this

Adds the value to the buffer, followed by a new line.

textBuilder.toString()


textBuilder.toString() → String

Returns the string.

Type type (joe)


The Type type is the root of the Joe type system, being the meta-type of all other types. Instances of Type are types themselves, and are created in a variety of ways, i.e., by declaring a class, or by implementing a Joe binding to a Java type.

Joe Test Tool API (joe.test)


This package defines the test assertions and checkers that are available for use in joe test test suites.

Functions

Types

Imported Types

In addition to the APIs documented here, joe.test includes types from other packages so that they can be tested:

Functions


assertEquals()


assertEquals(got, expected)

Verifies that got equals the expected value, producing an informative assertion error if not.

assertError()


assertError(callable, [message], [frames...])

Executes a callable expecting it to throw an error and failing the test if it does not. The error must have the given message and stack frames, if they are provided.

When stack frames are provided, the error must include at least the provided number of frame strings, and those strings must match.

assertFalse()


assertFalse(condition)

Verifies that condition is falsey, producing an informative assertion error if not.

assertTrue()


assertTrue(condition)

Verifies that condition is truthy, producing an informative assertion error if not.

check()


check(value) → ValueChecker

Returns a checker for testing a computed value.

checkCatch()


checkCatch(callable) → CatchChecker

Executes a no-arg callable and returns a checker for checking the error result.

engine()


engine() → String

Returns the name of the engine in use, "walker" or "bert".

fail()


fail(message)

Throws an assertion error with the given message, failing the test immediately.

skip()


skip(message)

Skips the current test with the given message without executing it further. The test will be counted as "Skipped" in the final test results.

CatchChecker type (joe.test)


A fluent test checker for catch() results. Use checkCatch(callable) to create it.

Initializer

Methods

CatchChecker Initializer


CatchChecker(catchResult) → CatchChecker

Initializes the checker with the result of a catch().

Methods


catchChecker.isError()


catchChecker.isError() → Boolean

Returns true if there was an error, and false otherwise.

catchChecker.isOK()


catchChecker.isOK() → this

Checks that the catch result is not an error.

catchChecker.message()


catchChecker.message(expected) → this

Checks that the error message matches the expected value.

catchChecker.stackFrames()


catchChecker.stackFrames(frame,...) → this

Checks that the actual stack frames match the expected list of stack frames. The expected frames are passed as individual arguments.

catchChecker.stackTrace()


catchChecker.stackTrace(expected) → this

Checks that the actual stack traces matches the expected stack trace string.

catchChecker.type()


catchChecker.type(expected) → this

Checks the error type matches the expected value.

JoeTest type (joe.test)


An API for use by test scripts.

Constants

Constants


JoeTest.OPAQUE


A value of an opaque type, for use in testing.

ValueChecker type (joe.test)


A fluent test checker for arbitrary values. Use check(value) to create one.

Initializer

Methods

ValueChecker Initializer


ValueChecker(value) → ValueChecker

Initializes the new checker with the given value.

Methods


valueChecker.containsAll()


valueChecker.containsAll(values...) → this

Assumes that the value is a collection, and verifies that it contains all of the expected arguments.

valueChecker.eq()


valueChecker.eq(expected) → this

Checks that the value is equal to the expected value.

valueChecker.hasType()


valueChecker.hasType(type) → this

Checks that the value has the given type.

valueChecker.hasTypeName()


valueChecker.hasTypeName(name) → this

Checks that the value's type has the given name.

valueChecker.isEmpty()


valueChecker.isEmpty() → this

Checks that the value isEmpty() according to its own isEmpty() method.

valueChecker.isFalse()


valueChecker.isFalse() → this

Checks that the value is falsey.

valueChecker.isNotNull()


valueChecker.isNotNull() → this

Checks that the value is not null

valueChecker.isNull()


valueChecker.isNull() → this

Checks that the value is null

valueChecker.isTrue()


valueChecker.isTrue() → this

Checks that the value is truthy.

JoeDoc Configuration API (joe.doc)


The joe.doc package contains the Joe API used in the joe doc configuration file, doc_config.joe.

Types

DocConfig type (joe.doc)


The DocConfig static type owns the static methods used to configure the joe doc tool.

Static Methods

Static Methods


DocConfig.inputFile()


DocConfig.inputFile(filename,...) → this

Adds the paths of one or more files to scan for JoeDoc comments. File paths are relative to the location of the doc_config.joe file.

DocConfig.inputFolder()


DocConfig.inputFolder(folder,...) → this

Adds the paths of one or more folders to scan for files containing JoeDoc comments. joe doc will scan all .java and .joe files in the folders, recursing down into subfolders.

Folder paths are relative to the location of the doc_config.joe file.

DocConfig.outputFolder()


DocConfig.outputFolder(folder) → this

Sets the name of the folder to receive the generated outputs. If unset, defaults to the folder containing the doc_config.joe file.

joe.console package


This package contains an API for use by simple command-line tools.

Types

Using it, clients can:

  • Acquire the command line arguments
  • Read from System.in
  • Read from a file
  • Write to a file
  • Query and set the current working directory

Console type (joe.console)


This static type provides a modicum of access to the console environment, sufficient to writing simple scripts.

Static Methods

Static Methods


Console.args()


Console.args() → List

Returns a list of the arguments passed to the command line, as filtered by the application.

Console.cd()


Console.cd(path) → Path

Changes the current working directory to the given path. If the path is relative, it is treated as relative to the current working directory.

Console.exit()


Console.exit([code])

Exits the application with the given exit code, which defaults to 0.

Console.mkdir()


Console.mkdir(path)

Creates the directory given its path, including any required parent directories. If the path is relative, it is treated as relative to the current working directory.

Console.pwd()


Console.pwd() → Path

Returns the current working directory.

Console.read()


Console.read() → String

Returns a string from standard input, or null at EOF.

Console.readFile()


Console.readFile(filePath) → String

Reads the contents of the file at the given filePath and returns it as a string.

Console.readLines()


Console.readLines(filePath) → List

Reads the contents of the file at the given filePath and returns it as a list of line strings

Console.writeFile()


Console.writeFile(filePath, text)

Writes the text as a file at the given filePath.

Path type (joe.console)


A Java Path, i.e, the path to a file or a directory in the host file system.

Static Methods

Initializer

Methods

Static Methods


Path.compare()


Path.compare(a, b) → Number

Returns -1, 0, or 1 as a is less than, equal to, or greater than b when compared lexicographically.

Path Initializer


Path(first,[more...]) → Path

Creates a path from one or more string components.

Methods


path.endsWith()


path.endsWith(path) → Boolean

Returns true if this path ends with the given path. The path may be specified as a Path or a String.

path.getFileName()


path.getFileName() → Path

Returns the final path component, i.e., the name of the file or directory denoted by this path.

path.getName()


path.getName(index) → Path

Returns the path component at the given index.

path.getNameCount()


path.getNameCount() → Number

Returns the number of path components.

path.getParent()


path.getParent() → Path

Returns the parent of this path, or null if it doesn't have one.

path.isAbsolute()


path.isAbsolute() → Boolean

Returns true if the path is an absolute path, and false otherwise.

path.normalize()


path.normalize() → Path

Returns the path with redundant name elements eliminated.

path.relativize()


path.relativize(other) → Path

Returns a relative path from this path to the other path.

The other path may a Path or a String.

path.resolve()


path.resolve(other) → Path

Resolves the other path against this path, if other is relative, constructs a path starting with this path and continuing with the other. If other is absolute, it is returned unchanged.

The other path may a Path or a String.

path.startsWith()


path.startsWith(path) → Boolean

Returns true if this path starts with the given path. The path may be specified as a Path or a String.

path.subpath()


path.subpath(start, [end]) → Path

Returns the subpath of this path that starts at start and ends before end, which defaults to the end of the path.

path.toAbsolutePath()


path.toAbsolutePath() → Path

Converts the path into an absolute path.

path.toString()


path.toString() → String

Returns the path's string representation.

joe.win package


This package contains an experimental API for building JavaFX GUIs. It assumes a familiarity with JavaFX.

Topics

Types

Widget Hierarchy


The joe.win widget type hierarchy is a subset of the JavaFX hierarchy.

Styling with CSS


Scripts can style widgets using CSS in several different ways.

  • The Win.css() method associates a CSS stylesheet, passed as a text string, with the application as a whole.
  • Each widget has a styleClasses() method which returns a list of the names of the CSS style classes that apply to the widget; add any desired style class name to this list.
  • Each widget has a #style property which can be set to a string defining one or more CSS style settings, similar to the HTML style attribute.

JavaFX styles widgets using its own peculiar form of CSS, called "FXCSS". See the JavaFX CSS Reference Guide for details.

Control type (joe.win)


Extends: Region
Extended By: SplitPane, Button, TabPane, MenuBar, Separator, Label, ListView

The Control type is the base class for JavaFX controls like the Label and Button widgets.

Methods

Region Methods

Node Methods

Properties

Control widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#tooltip[[Tooltip]]The control's tooltip

Methods


control.tooltip()


control.tooltip(tooltip) → this

Sets the control's [[Tooltip]], or null for none.

control.tooltipText()


control.tooltipText(text) → this

Gives the control a tooltip with the given text.

Button type (joe.win)


Extends: Control

The Button type is the base class for JavaFX labels like Button widgets.

Initializer

Methods

Control Methods

Region Methods

Node Methods

Properties

Button widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#onActioncallable(1)The onAction handler
#textStringThe button's text
  • callable(1): A callable taking one argument

Button Initializer


Button([text], [action]) → Button

Returns a Button. If the text is given, the button will display the text. If the action is also given, it must be a no-arg callable; it will be invoked when the button is pressed.

Methods


button.action()


button.action(callable) → this

Adds a no-arg callable to the button as its action; pressing the button will invoke the callable.

button.text()


button.text(text) → this

Sets the button's text.

GridPane type (joe.win)


Extends: Pane

The GridPane type is a Pane that manages one or children stacked one on top of each other like cards in a deck. Joe classes can extend the GridPane type.

Initializer

Methods

Pane Methods

Region Methods

Node Methods

Properties

GridPane widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#alignmentPosAlignment of the grid within the widget.
#gridLinesVisibleBooleanWhether to draw grid lines for debugging.
#hgapNumberGap between columns in pixels.
#vgapNumberGap between rows in pixels.

GridPane Initializer


GridPane() → GridPane

Returns a GridPane.

Methods


gridPane.at()


gridPane.at(column, row, [columnSpan, rowSpan], node) → this

Adds the node to the grid pane at the given column and row with the given spans. If omitted, the spans default to 1.

gridPane.hgap()


gridPane.hgap(pixels) → this

Sets the gap between columns to the given number of pixels.

gridPane.vgap()


gridPane.vgap(pixels) → this

Sets the gap between rows to the given number of pixels.

HBox type (joe.win)


Extends: Pane

The HBox type is a Pane that manages a row of widgets. Joe classes can extend the HBox type.

Static Methods

Initializer

Methods

Pane Methods

Region Methods

Node Methods

Properties

HBox widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#alignmentPosThe default alignment for children
#spacingNumberThe spacing between children in pixels

Static Methods


HBox.getHgrow()


HBox.getHgrow(node) → Priority

Gets how the Node will resize itself to the height of its parent HBox.

HBox.getMargin()


HBox.getMargin(node) → Insets

Gets the Node's margin in its parent HBox.

HBox.setHgrow()


HBox.setHgrow(node, priority)

Sets how the Node will resize itself to the height of its parent HBox, given a Priority value.

HBox.setMargin()


HBox.setMargin(node, insets)

Gets the Node's margin in its parent HBox given an Insets object.

HBox Initializer


HBox() → HBox

Returns a HBox.

Methods


hBox.spacing()


hBox.spacing(pixels) → this

Sets the vertical space in pixels between each child.

HPos type (joe.win)


Constants

Static Methods

Initializer

Methods

Constants


HPos.CENTER


HPos.LEFT


HPos.RIGHT


Static Methods


HPos.values()


HPos.values() → List

Returns a list of the enumerated type's values.

HPos Initializer


HPos(value) → HPos

Attempts to convert the value to an enum constant for this enum. The value may be:

  • One of the enum's constants.
  • A string that matches the name of a constant, disregarding case.
  • A keyword whose name matches the name of a constant, disregarding case.

Methods


hPos.name()


hPos.name() → String

Returns the name of the enumerated constant.

hPos.ordinal()


hPos.ordinal() → Number

Returns the index of the enumerated constant in the values() list.

hPos.toString()


hPos.toString() → String

Returns the name of the enumerated constant.

Insets type (joe.win)


The Insets type used to set margins and padding around Node widgets.

Initializer

Methods

Insets Initializer


Insets(pixels) → Insets
Insets(top, right, bottom, left) → Insets

Returns a Insets, which represents a margin on the four sides of a Node. If a single value pixels is given, the margin will be the same on all four sides; otherwise, the initializer expects all four values in the given order.

Methods


insets.getBottom()


insets.getBottom() → Number

Gets the width of the margin on the bottom of the node, in pixels.

insets.getLeft()


insets.getLeft() → Number

Gets the width of the margin on the left of the node, in pixels.

insets.getRight()


insets.getRight() → Number

Gets the width of the margin on the right of the node, in pixels.

insets.getTop()


insets.getTop() → Number

Gets the width of the margin on the top of the node, in pixels.

Label type (joe.win)


Extends: Control

The Label type is the base class for JavaFX labels like Label widgets.

Initializer

Methods

Control Methods

Region Methods

Node Methods

Properties

Label widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#textStringThe label's text

Label Initializer


Label([text]) → Label

Returns a Label.

Methods


label.text()


label.text(text) → this

Sets the label's text.

ListView type (joe.win)


Extends: Control

The ListView type is a JavaFX scrolling list widget. Joe classes can extend the ListView type.

Initializer

Methods

Control Methods

Region Methods

Node Methods

Properties

ListViewInstance widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#placeholderNodeEmpty list message

ListView Initializer


ListView() → ListView

Returns a ListView.

Methods


listView.getItems()


listView.getItems() → joe.List

Gets the list of the widget's items, which can be updated freely.

listView.getSelectedIndex()


listView.getSelectedIndex() → joe.Number

Gets the index of the selected item, or -1 if there is no selection.

listView.getSelectedItem()


listView.getSelectedItem() → item

Gets the selected item, or null if there is no selection.

listView.item()


listView.item(item) → this

Adds a value to the widget's list of items.

listView.items()


listView.items(list) → this

Adds the contents of the list to the widgets list of items.

listView.onSelect()


listView.onSelect(callable) → this

Specifies a callable to be called when the user selects an item in the list. The callable must take one argument, the ListView itself.

listView.placeholder()


listView.placeholder(node) → this

Sets the widget's placeholder graphic, a Node to display when the widget's items() list is empty.

listView.placeholderText()


listView.placeholderText(text) → this

Sets the widget's placeholder graphic to a label displaying the given text. The placeholder is shown when the widget's items() list is empty.

listView.selectIndex()


listView.selectIndex(index) → this

Selects the item at the given index. Throws an error if the index is not in range.

listView.selectItem()


listView.selectItem(item) → this

Selects the given item. The call is a no-op if the item isn't contained in the widget's list of items.

listView.stringifier()


listView.stringifier(callable) → this

Sets the widget's stringifier to the given callable, which must take one argument, a list item, and return a string representation for that item.

Menu type (joe.win)


The Menu widget is a menu in a MenuBar or a submenu in a parent Menu. It contains MenuItem widgets.

Initializer

Methods

Properties

All Node widgets have the following properties.

PropertyTypeDescription
#idStringJavaFX ID
#styleStringFXCSS style string
#textStringMenu text

See Styling with CSS for more on using CSS.

Menu Initializer


Menu() → Menu

Returns a Menu.

Methods



menu.disable([flag]) → this

Sets the menu's #disable property to flag; if omitted, flag defaults to true. While true, this menu and its descendants in the scene graph will be disabled.


menu.getProperty(keyword) → value

Gets the value of the property with the given keyword.


menu.id(id) → this

Sets the menu's #id property to the given id string.


menu.isDisabled() → joe.Boolean

Returns true if the menu has been disabled, and false otherwise.


menu.item(item) → this

Adds a MenuItem or Menu to the menu.


menu.items() → joe.List

Gets the list of the menu's items, which can be updated freely. All items must be instances of MenuItem or Menu.


menu.listenTo(keyword, listener) → this

Adds a listener to the property with the given keyword. The listener should be a callable taking the three arguments:

  • The property keyword
  • The old value of the property
  • The new value of the property

The listener will be called when the property's value changes.


menu.properties() → joe.Set

Returns a readonly Set of the object's property keywords.


menu.setProperty(keyword, value) → this

Sets the value of the property with the given keyword. The value must be assignable to the property's value type.


menu.styleClasses() → joe.List

Gets the list of the menu's FXCSS style class names. Values must be valid CSS style class names.

See Styling with CSS for more on using CSS.


menu.styles(style, ...) → this

Sets the menu's FXCSS #style property. The caller can pass multiple style strings, which will be joined with semicolons.

See Styling with CSS for more on using CSS.


menu.text(text) → this

Sets the menu's text.


menu.toString() → joe.String

Returns the value's string representation.

MenuBar type (joe.win)


Extends: Control

The MenuBar type is the base class for JavaFX labels like MenuBar widgets.

Initializer

Methods

Control Methods

Region Methods

Node Methods

Properties

MenuBar widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#useSystemMenuBarBooleanUse system menu bar.

MenuBar Initializer


MenuBar([text]) → MenuBar

Returns a MenuBar.

Methods



menuBar.menu(menu) → this

Adds a Menu to the menu bar.


menuBar.menus() → joe.List

Gets the list of the menu bar's menus, which can be updated freely. All items must be instances of Menu.

MenuItem type (joe.win)


The MenuItem widget is an item in a Menu.

Initializer

Methods

Properties

All Node widgets have the following properties.

PropertyTypeDescription
#idStringJavaFX ID
#onActioncallable(1)The onAction handler
#styleStringFXCSS style string
#textStringMenuItem text

See Styling with CSS for more on using CSS.

MenuItem Initializer


MenuItem() → MenuItem

Returns a MenuItem.

Methods



menuItem.action(callable) → this

Adds a no-arg callable to the menu item as its action; pressing the button will invoke the callable.


menuItem.disable([flag]) → this

Sets the item's #disable property to flag; if omitted, flag defaults to true. While true, this item and its descendants in the scene graph will be disabled.


menuItem.getProperty(keyword) → value

Gets the value of the property with the given keyword.


menuItem.id(id) → this

Sets the item's #id property to the given id string.


menuItem.isDisabled() → joe.Boolean

Returns true if the item has been disabled, and false otherwise.


menuItem.listenTo(keyword, listener) → this

Adds a listener to the property with the given keyword. The listener should be a callable taking the three arguments:

  • The property keyword
  • The old value of the property
  • The new value of the property

The listener will be called when the property's value changes.


menuItem.properties() → joe.Set

Returns a readonly Set of the object's property keywords.


menuItem.setProperty(keyword, value) → this

Sets the value of the property with the given keyword. The value must be assignable to the property's value type.


menuItem.styleClasses() → joe.List

Gets the list of the item's FXCSS style class names. Values must be valid CSS style class names.

See Styling with CSS for more on using CSS.


menuItem.styles(style, ...) → this

Sets the item's FXCSS #style property. The caller can pass multiple style strings, which will be joined with semicolons.

See Styling with CSS for more on using CSS.


menuItem.text(text) → this

Sets the item's text.


menuItem.toString() → joe.String

Returns the value's string representation.

Node type (joe.win)


Extended By: Region

The Node widget is the abstract base class for the JavaFX widget hierarchy. This abstract type provides features available for all widgets.

Methods

Properties

All Node widgets have the following properties.

PropertyTypeDescription
#idStringJavaFX ID
#styleStringFXCSS style string
#visibleBooleanVisibility flag

See Styling with CSS for more on using CSS.

Methods


node.disable()


node.disable([flag]) → this

Sets the node's #disable property to flag; if omitted, flag defaults to true. While true, this node and its descendants in the scene graph will be disabled.

node.getProperty()


node.getProperty(keyword) → value

Gets the value of the property with the given keyword.

node.gridColumn()


node.gridColumn(index) → this

Sets the GridPane column constraint for the node to the given column index.

This method is equivalent to the JavaFX GridPane.setColumnIndex() method.

node.gridColumnSpan()


node.gridColumnSpan(span) → this

Sets the GridPane columnSpan constraint for the node to the given span, which must be a positive number.

This method is equivalent to the JavaFX GridPane.setColumnSpan() method.

node.gridHalignment()


node.gridHalignment(hpos) → this

Sets the GridPane halignment constraint for the node to the given HPos.

This method is equivalent to the JavaFX GridPane.setHalignment() method.

node.gridHgrow()


node.gridHgrow([priority]) → this

Sets the GridPane hgrow constraint for the node to the given Priority, or to Priority.ALWAYS if the priority is omitted.

This method is equivalent to the JavaFX GridPane.setHgrow() method.

node.gridMargin()


node.gridMargin(insets) → this

Sets the GridPane margin constraint for the node to the given Insets.

This method is equivalent to the JavaFX GridPane.setMargin() method.

node.gridRow()


node.gridRow(index) → this

Sets the GridPane row constraint for the node to the given row index.

This method is equivalent to the JavaFX GridPane.setRowIndex() method.

node.gridRowSpan()


node.gridRowSpan(span) → this

Sets the GridPane rowSpan constraint for the node to the given span, which must be a positive number.

This method is equivalent to the JavaFX GridPane.setRowSpan() method.

node.gridValignment()


node.gridValignment(vpos) → this

Sets the GridPane valignment constraint for the node to the given VPos.

This method is equivalent to the JavaFX GridPane.setValignment() method.

node.gridVgrow()


node.gridVgrow([priority]) → this

Sets the GridPane vgrow constraint for the node to the given Priority, or to Priority.ALWAYS if the priority is omitted.

This method is equivalent to the JavaFX GridPane.setVgrow() method.

node.hgrow()


node.hgrow([priority]) → this

Sets the HBox hgrow constraint for the node to the given Priority, or to Priority.ALWAYS if the priority is omitted.

This method is equivalent to the JavaFX HBox.setHgrow() method.

node.id()


node.id(id) → this

Sets the node's #id property to the given id string.

node.isDisabled()


node.isDisabled() → joe.Boolean

Returns true if the node has been disabled, and false otherwise.

node.listenTo()


node.listenTo(keyword, listener) → this

Adds a listener to the property with the given keyword. The listener should be a callable taking the three arguments:

  • The property keyword
  • The old value of the property
  • The new value of the property

The listener will be called when the property's value changes.

node.properties()


node.properties() → joe.Set

Returns a readonly Set of the object's property keywords.

node.setProperty()


node.setProperty(keyword, value) → this

Sets the value of the property with the given keyword. The value must be assignable to the property's value type.

node.splitResizeWithParent()


node.splitResizeWithParent(flag) → this

If flag is true (the default value) the node's "split" will resize when its parent SplitPane is resized, preserving its divider fraction. If false, the divider fraction will change to keep the node's width or height constant. Use this to prevent sidebars from resizing when the window is resized.

This is equivalent to the JavaFX SplitPane.setResizeWithParent() method.

node.styleClasses()


node.styleClasses() → joe.List

Gets the list of the node's FXCSS style class names. Values must be valid CSS style class names.

See Styling with CSS for more on using CSS.

node.styles()


node.styles(style, ...) → this

Sets the node's FXCSS #style property. The caller can pass multiple style strings, which will be joined with semicolons.

See Styling with CSS for more on using CSS.

node.toString()


node.toString() → joe.String

Returns the value's string representation.

node.vgrow()


node.vgrow([priority]) → this

Sets the VBox vgrow constraint for the node to the given Priority, or to Priority.ALWAYS if the priority is omitted.

This method is equivalent to the JavaFX VBox.setVgrow() method.

Orientation type (joe.win)


Constants

Static Methods

Initializer

Methods

Constants


Orientation.HORIZONTAL


Orientation.VERTICAL


Static Methods


Orientation.values()


Orientation.values() → List

Returns a list of the enumerated type's values.

Orientation Initializer


Orientation(value) → Orientation

Attempts to convert the value to an enum constant for this enum. The value may be:

  • One of the enum's constants.
  • A string that matches the name of a constant, disregarding case.
  • A keyword whose name matches the name of a constant, disregarding case.

Methods


orientation.name()


orientation.name() → String

Returns the name of the enumerated constant.

orientation.ordinal()


orientation.ordinal() → Number

Returns the index of the enumerated constant in the values() list.

orientation.toString()


orientation.toString() → String

Returns the name of the enumerated constant.

Pane type (joe.win)


Extends: Region
Extended By: VBox, HBox, GridPane, StackPane

The Pane type is the base class for JavaFX Node widgets that manage child nodes.

Initializer

Methods

Region Methods

Node Methods

Properties

Pane widgets have the properties they inherit from their superclasses.

Pane Initializer


Pane() → Pane

Returns a Pane.

Methods


pane.child()


pane.child(node) → this

Adds a Node to the end of the pane's children list.

pane.children()


pane.children() → joe.List

Gets the list of the node's children, which can be updated freely. All items must belong some Node subclass.

Pos type (joe.win)


The Pos enum lists ways a widget can be aligned relative to the boundaries of a rectangular space.

Constants

Static Methods

Initializer

Methods

Constants


Pos.BASELINE_CENTER


Pos.BASELINE_LEFT


Pos.BASELINE_RIGHT


Pos.BOTTOM_CENTER


Pos.BOTTOM_LEFT


Pos.BOTTOM_RIGHT


Pos.CENTER


Pos.CENTER_LEFT


Pos.CENTER_RIGHT


Pos.TOP_CENTER


Pos.TOP_LEFT


Pos.TOP_RIGHT


Static Methods


Pos.values()


Pos.values() → List

Returns a list of the enumerated type's values.

Pos Initializer


Pos(value) → Pos

Attempts to convert the value to an enum constant for this enum. The value may be:

  • One of the enum's constants.
  • A string that matches the name of a constant, disregarding case.
  • A keyword whose name matches the name of a constant, disregarding case.

Methods


pos.name()


pos.name() → String

Returns the name of the enumerated constant.

pos.ordinal()


pos.ordinal() → Number

Returns the index of the enumerated constant in the values() list.

pos.toString()


pos.toString() → String

Returns the name of the enumerated constant.

Priority type (joe.win)


The Priority enum's values indicate when a widget should resize itself to fit its parent widget. The default is generally NEVER.

Constants

Static Methods

Initializer

Methods

Constants


Priority.ALWAYS


Priority.NEVER


Priority.SOMETIMES


Static Methods


Priority.values()


Priority.values() → List

Returns a list of the enumerated type's values.

Priority Initializer


Priority(value) → Priority

Attempts to convert the value to an enum constant for this enum. The value may be:

  • One of the enum's constants.
  • A string that matches the name of a constant, disregarding case.
  • A keyword whose name matches the name of a constant, disregarding case.

Methods


priority.name()


priority.name() → String

Returns the name of the enumerated constant.

priority.ordinal()


priority.ordinal() → Number

Returns the index of the enumerated constant in the values() list.

priority.toString()


priority.toString() → String

Returns the name of the enumerated constant.

Region type (joe.win)


Extends: Node
Extended By: Control, Pane

The Region type is the abstract base class for JavaFX Node widgets that occupy space on the screen.

Constants

Methods

Node Methods

Constants


Region.USE_COMPUTED_SIZE


Default setting for the region's various width and height properties.

Region.USE_PREF_SIZE


Use as the #minWidth, #maxWidth, #minHeight, or #maxHeight value to indicate that the preferred width or height should be used for that property.

Properties

Region widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#maxHeightNumberMaximum height in pixels
#maxWidthNumberMaximum width in pixels
#minHeightNumberMinimum height in pixels
#minWidthNumberMinimum width in pixels
#paddingInsetsPreferred height in pixels
#prefHeightNumberPreferred height in pixels
#prefWidthNumberPreferred width in pixels

Methods


region.height()


region.height(height) → this

Sets the node's preferred, minimum, and maximum height in pixels.

region.padding()


region.padding(pixels) → this
region.padding(top, right, bottom, left) → this

Sets the padding in pixels on all sides of the region. If a single value is given, it is used for all four sides.

region.prefHeight()


region.prefHeight(height) → this

Sets the node's preferred height in pixels.

region.prefWidth()


region.prefWidth(width) → this

Sets the node's preferred width in pixels.

region.width()


region.width(width) → this

Sets the node's preferred, minimum, and maximum width in pixels.

Separator type (joe.win)


Extends: Control

The Separator widget is a horizontal or vertical separator.

Initializer

Methods

Control Methods

Region Methods

Node Methods

Properties

Separator widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#orientationOrientationHorizontal or vertical

Separator Initializer


Separator() → Separator

Returns a Separator, which is horizontal by default.

Methods


separator.horizontal()


separator.horizontal() → this

Sets the orientation to horizontal

separator.vertical()


separator.vertical() → this

Sets the orientation to vertical

Side type (joe.win)


A Side of a rectangular region.

Constants

Static Methods

Initializer

Methods

Constants


Side.BOTTOM


Side.LEFT


Side.RIGHT


Side.TOP


Static Methods


Side.values()


Side.values() → List

Returns a list of the enumerated type's values.

Side Initializer


Side(value) → Side

Attempts to convert the value to an enum constant for this enum. The value may be:

  • One of the enum's constants.
  • A string that matches the name of a constant, disregarding case.
  • A keyword whose name matches the name of a constant, disregarding case.

Methods


side.name()


side.name() → String

Returns the name of the enumerated constant.

side.ordinal()


side.ordinal() → Number

Returns the index of the enumerated constant in the values() list.

side.toString()


side.toString() → String

Returns the name of the enumerated constant.

SplitPane type (joe.win)


Extends: Control

The SplitPane type manages one or more Node widgets with movable dividers between them.

Initializer

Methods

Control Methods

Region Methods

Node Methods

If there are N children, then there are N-1 dividers between them, indexed from 0 to N-2. The divider positions are fractions between 0.0 and 1.0.

Properties

SplitPane widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#orientationOrientationLayout orientation

SplitPane Initializer


SplitPane() → SplitPane

Returns a SplitPane.

Methods


splitPane.getDividers()


splitPane.getDividers() → joe.List

Returns a list of the divider positions, which are fractions between 0.0 and 1.0.

splitPane.horizontal()


splitPane.horizontal() → this

Sets the orientation to horizontal

splitPane.item()


splitPane.item(item) → this

Adds a value to the widget's list of Node widgets.

splitPane.items()


splitPane.items() → joe.List

Gets the list of the widget's items, which can be updated freely. All items must be Node widgets.

splitPane.setDivider()


splitPane.setDivider(index position) → this

Sets the value of divider with the given index to the given position.

splitPane.setDividers()


splitPane.setDividers(position,...) → this

Sets the value of all dividers to the given positions.

splitPane.vertical()


splitPane.vertical() → this

Sets the orientation to vertical

StackPane type (joe.win)


Extends: Pane

The StackPane type is a Pane that manages one or children stacked one on top of each other like cards in a deck. Joe classes can extend the StackPane type.

Static Methods

Initializer

Pane Methods

Region Methods

Node Methods

Properties

StackPane widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#alignmentPosThe default alignment for children

Static Methods


StackPane.getAlignment()


StackPane.getAlignment(node) → Pos

Gets the Node's alignment in its parent StackPane.

StackPane.getMargin()


StackPane.getMargin(node) → Insets

Gets the Node's margin in its parent StackPane.

StackPane.setAlignment()


StackPane.setAlignment(node, pos)

Sets how Node node will position itself within its parent StackPane, given a Pos value.

StackPane.setMargin()


StackPane.setMargin(node, insets)

Gets the Node's margin in its parent StackPane given an Insets object.

StackPane Initializer


StackPane() → StackPane

Returns a StackPane.

Tab type (joe.win)


The Tab widget contains a Node in a TabPane. Joe classes can extend the Tab type.

Initializer

Methods

Properties

All Node widgets have the following properties.

PropertyTypeDescription
#contentNodeContent node
#idStringJavaFX ID
#styleStringFXCSS style string
#textStringTab text

See Styling with CSS for more on using CSS.

Tab Initializer


Tab() → Tab

Returns a Tab.

Methods


tab.content()


tab.content(node) → this

Sets the tab's #content property to node.

tab.disable()


tab.disable([flag]) → this

Sets the tab's #disable property to flag; if omitted, flag defaults to true. While true, this tab and its descendants in the scene graph will be disabled.

tab.getProperty()


tab.getProperty(keyword) → value

Gets the value of the property with the given keyword.

tab.id()


tab.id(id) → this

Sets the tab's #id property to the given id string.

tab.isDisabled()


tab.isDisabled() → joe.Boolean

Returns true if the tab has been disabled, and false otherwise.

tab.isSelected()


tab.isSelected() → joe.Boolean

Returns true if the tab is selected, and false otherwise.

tab.listenTo()


tab.listenTo(keyword, listener) → this

Adds a listener to the property with the given keyword. The listener should be a callable taking the three arguments:

  • The property keyword
  • The old value of the property
  • The new value of the property

The listener will be called when the property's value changes.

tab.properties()


tab.properties() → joe.Set

Returns a readonly Set of the object's property keywords.

tab.setProperty()


tab.setProperty(keyword, value) → this

Sets the value of the property with the given keyword. The value must be assignable to the property's value type.

tab.styleClasses()


tab.styleClasses() → joe.List

Gets the list of the tab's FXCSS style class names. Values must be valid CSS style class names.

See Styling with CSS for more on using CSS.

tab.styles()


tab.styles(style, ...) → this

Sets the tab's FXCSS #style property. The caller can pass multiple style strings, which will be joined with semicolons.

See Styling with CSS for more on using CSS.

tab.tabPane()


tab.tabPane() → TabPane

Returns the TabPane to which this tab belongs, or null if none.

tab.text()


tab.text(text) → this

Sets the label's text.

tab.toString()


tab.toString() → joe.String

Returns the value's string representation.

TabPane type (joe.win)


Extends: Control

The TabPane type is the base class for JavaFX tab panes, which can contain Tab objects.

Initializer

Methods

Control Methods

Region Methods

Node Methods

Properties

TabPane widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#sideSideThe position for the tabs.

TabPane Initializer


TabPane([text]) → TabPane

Returns a TabPane.

Methods


tabPane.tab()


tabPane.tab(tab) → this

Adds a Tab to the pane.

tabPane.tabs()


tabPane.tabs() → joe.List

Gets the list of the pane's tabs, which can be updated freely. All items must be instances of Tab or a subclass.

VBox type (joe.win)


Extends: Pane

The VBox type is a Pane that manages a column of widgets. Joe classes can extend the VBox type.

Static Methods

Initializer

Methods

Pane Methods

Region Methods

Node Methods

Properties

VBox widgets have the following properties, in addition to those inherited from superclasses.

PropertyTypeDescription
#alignmentPosThe default alignment for children
#spacingNumberThe spacing between children in pixels

Static Methods


VBox.getMargin()


VBox.getMargin(node) → Insets

Gets the Node's margin in its parent VBox.

VBox.getVgrow()


VBox.getVgrow(node) → Priority

Gets how the Node will resize itself to the height of its parent VBox.

VBox.setMargin()


VBox.setMargin(node, insets)

Gets the Node's margin in its parent VBox given an Insets object.

VBox.setVgrow()


VBox.setVgrow(node, priority)

Sets how the Node will resize itself to the height of its parent VBox, given a Priority value.

VBox Initializer


VBox() → VBox

Returns a VBox.

Methods


vBox.spacing()


vBox.spacing(pixels) → this

Sets the vertical space in pixels between each child.

VPos type (joe.win)


Constants

Static Methods

Initializer

Methods

Constants


VPos.BASELINE


VPos.BOTTOM


VPos.CENTER


VPos.TOP


Static Methods


VPos.values()


VPos.values() → List

Returns a list of the enumerated type's values.

VPos Initializer


VPos(value) → VPos

Attempts to convert the value to an enum constant for this enum. The value may be:

  • One of the enum's constants.
  • A string that matches the name of a constant, disregarding case.
  • A keyword whose name matches the name of a constant, disregarding case.

Methods


vPos.name()


vPos.name() → String

Returns the name of the enumerated constant.

vPos.ordinal()


vPos.ordinal() → Number

Returns the index of the enumerated constant in the values() list.

vPos.toString()


vPos.toString() → String

Returns the name of the enumerated constant.

Win type (joe.win)


This static type provides access to the application window.

Static Methods

Static Methods


Win.css()


Win.css(css) → this

Sets the text of the CSS style sheet for the application as a whole to css. For example,

Win.css("""
    .label { -fx-text-fill: pink; }
    """);

See Styling with CSS for more on using CSS in joe win scripts.

JavaFX: In particular, this adds the given CSS to the Scene's stylesheets property as a data: URL containing the given css text. The styles are therefore accessible to the entire scene.

Win.cssFile()


Win.cssFile(filename) → this

Sets the CSS style sheet for the application as a whole given a path to a .css file.

Win.cssFill("my.css");

See Styling with CSS for more on using CSS in joe win scripts.

JavaFX: In particular, this adds the given CSS file to the Scene's stylesheets property as a file: URL. The styles are therefore accessible to // the entire scene.

Win.root()


Win.root() → VBox

Returns the root window, a VBox.

Win.setSize()


Win.setSize(width, height) → this

Sets the preferred size of the root window. The width and height must be positive.

Win.setTitle()


Win.setTitle(title) → this

Sets the title of the root window.

Library API Index


The following is a complete index of the packages, functions, types, methods, and constants include in Joe's aPackage.

Appendix: Joe Grammar

This appendix shows the grammar of the Joe language, in the syntax used in Crafting Interpreters. Below that is the full JLox grammar.

Grammar

Here is the grammar; see Semantic Constraints for some conditions enforced by the compiler.

Statements

program         → declaration* EOF ;

declaration     → classDecl
                | funDecl
                | varDecl
                | statement ;

classDecl       | "class" IDENTIFIER ( "extends" IDENTIFIER )?
                  "{" classItem* "}" ;
classItem       → "method" function
                | "static" "method" function
                | "static" block ; 
                
funDecl         → "function" function ;
function        → IDENTIFIER "(" parameters? ")" block ;
parameters      → IDENTIFIER ( "," IDENTIFIER )* ;

recordDecl      → "record" IDENTIFIER "(" parameters ")" 
                  "{" recordItem* "}" ;
recordItem      → "method" function
                | "static" "method" function
                | "static" block ; 
                  
varDecl         → "var" IDENTIFIER ( "=" expression )? ";" 
                | "var" pattern "=" expression ";" ;
               
statement       → exprStmt
                | breakStmt
                | continueStmt
                | forStmt
                | foreachStmt
                | ifStmt
                | matchStmt
                | printStmt
                | returnStmt
                | switchStmt
                | throwStmt
                | whileStmt 
                | block ;

exprStmt        → expression ";" ;
breakStmt       → "break" ";" ;
continueStmt    → "continue" ";" ;
forStmt         → "for" "(" ( varDecl | exprStmt | ";" )
                  expression? ";"
                  expression? ")" statement ;
forEachStmt     → "foreach" "(" pattern ":" expression ")" statement;
ifStmt          → "if" "(" expression ")" statement 
                  ( "else" statement )? ;
matchStmt       → "match" "(" expression ")" "{" 
                  ( "case" pattern ( "if" expression )? "->" statement )+ 
                  ( "default" "->" statement )? 
                  "}" ;                  
printStmt       → "print" expression ";" ;
returnStmt      → "return" expression? ";" ;
switchStmt      → "switch" "(" expression ")" "{"
                  ( "case" expression ( "," expression )* "->" statement )+
                  ( "default" "->" statement )?
                  "}" ;
throwStmt       → "throw" expression ";" ;
whileStmt       → "while" "(" expression ")" statement ;
block           → "{" declaration* "}" ;

Expressions

expression      → assignment ;
assignment      → ( ( call "." )? IDENTIFIER 
                    | call "[" primary "]"
                  )
                  ( "=" | "+=" | "-=" | "*=" | "/=" ) 
                  assignment 
                | ternary ;
ternary         → logic_or "?" logic_or ":" logic_or ;
logic_or        → logic_and ( "||" logic_and )* ;
logic_and       → equality ( "&&" equality )* ;
equality        → comparison ( ( "!=" | "==" ) comparison )* ;
comparison      → term ( ( ">" | ">=" | "<" | "<=" | "~" | "in" | "ni" ) term )* ;
term            → factor ( ( "-" | "+" ) factor )* ;
factor          → unary ( ( "/" | "*" ) unary )* ;
unary           → ( "++" | "==" | "-" | "!" ) unary 
                | postfix ;
postfix         → call ( "++" | "--" )? ;
call            → primary ( "(" arguments? ")" 
                          | "." IDENTIFIER     
                          | "[" primary "]"
                  )*  ;
arguments       → expression ( "," expression )* ;
primary         → "true" | "false" | "nil"
                | NUMBER | STRING | KEYWORD
                | "this"
                | "@" IDENTIFIER
                | "ruleset" "{" ruleset "}" ;
                | IDENTIFIER 
                | lambda 
                | grouping
                | list 
                | "super" "." IDENTIFIER ;
grouping        → "(" expression ")"
lambda          → "\" parameters? "->" ( expression | block ) ; 
list            → "[" (expression ( "," expression )* ","? )? "]" ;
map             → "{" (map_entry ( "," map_entry )* ","? )? "]" ;
map_entry       → expression ":" expression ;

Patterns

A pattern is a destructuring pattern that can be used with the ~ operator, and with a number of Joe statements.

pattern         → patternBinding
                | constantPattern
                | wildcardPattern
                | valueBinding
                | listPattern
                | mapPattern 
                | namedFieldPattern 
                | orderedFieldPattern ;
                

A patternBinding is a pattern that binds a subpattern's matched value to a binding variable. patternBindings are disallows as the top pattern in some contexts.

patternBinding   | IDENTIFIER "@" pattern ;

A constantPattern matches a literal or computed constant. Variables referenced in a constantPattern must be defined in the enclosing scope and not shadowed by one of the pattern's binding variables.

constantPattern → "true" | "false" | "null" | STRING | NUMBER | KEYWORD
                | "$" IDENTIFIER
                | "$" "(" expression ")" ;

A valueBinding binds the matched value, whatever it is, to a variable.

valueBinding    → IDENTIFIER ;

A listPattern is a list of subpatterns to be matched against the items of a list, with an optional binding variable for the tail of the list.

listPattern     → "[" 
                      (subpattern ( "," subpattern )* ","? )?
                      ( ":" IDENTIFIER )?
                  "]";

A mapPattern is a map of key patterns and value patterns to be matched against the entries of a map value. All keys in the pattern must appear in the map, but the pattern need not exhaust the map. The key patterns must be constantPatterns; Joe does not attempt to do full pattern matching on the map keys.

mapPattern      → "{"
                      ( entryPattern ( "," entryPattern )* ","? )?
                  "}" ;
entryPattern    → constantPattern ":" subpattern ;

A namedFieldPattern matches the type and fields of a Joe value, matching fields by name rather than by position. The first IDENTIFIER must match the value's type name.

namedFieldPattern → IDENTIFIER "("
                      ( fieldPattern ( "," fieldPattern )* ","? )?
                  ")" ;
fieldPattern    → IDENTIFIER ":" subpattern ;

An orderedFieldPattern matches the type and fields of a Joe object that has ordered fields (e.g., Joe records). The fields are matched by position rather than by name in a list-pattern-like fashion.

orderedFieldPattern  → IDENTIFIER "(" 
                           ( pattern ( "," pattern )* ","? )? 
                       ")" ;

Nero

This is the grammar for Nero, Joe's dialect of Datalog. Nero rule sets can be parsed standalone, or as part of a Joe script via the ruleset expression.

Differences from classic Datalog with negation:

  • Comments begin with // rather than %
  • Horn clauses end with ; rather than '.'.
  • Relations usually have initial caps, to match Monica type names.
  • Constant terms can be Monica scalar values, or, if read from a scripted input fact, any Monica value.
  • Variables are normal identifiers, usually lowercase.
  • Wildcards are identifiers with a leading _.
  • Constraints follow a where token.
  • Body atoms can reference fact fields either by name or by position.
nero        → clause* EOF ;
ruleset     → ( clause | export )* EOF ;
export      → "export" IDENTIFIER ( "as" expression )? ";" ;
clause      → axiom
            | rule ;
axiom       → head ";"
rule        → head ":-" body ( "where" constraints )? ";"
head        → indexedAtom ;
body        → "not"? bodyAtom ( "," "not"? bodyAtom )* ;
bodyAtom    → indexedAtom | namedAtom ;
indexedAtom → IDENTIFIER "(" term ( "," term )* ")" ;
orderedAtom → IDENTIFIER "(" namedTerm ( "," namedTerm )* ")" ;
namedTerm   → IDENTIFIER ":" term ;
constrants  → constraint ( "," constraint )* ;
constraint  → variable ( "==" | "!=" | ">" | ">=" | "<" | "<=" ) term ;
term        → constant | variable | wildcard ;
constant    → KEYWORD | STRING | NUMBER | TRUE | FALSE | NULL ;
variable    → IDENTIFIER ;     // No leading "_"
wildcard    → IDENTIFIER ;     // With leading "_"

Semantic restrictions:

  • Normal body atoms can contain free variables to bind against facts. All variables in a rule's head, negated body atoms, or constraints must be bound in normal body atoms.
  • Only body atoms (negated or otherwise) may contain wildcard terms.

JLox Grammar

Here is Nystrom's JLox grammar, for comparison.

// Statements
program         → declaration* EOF ;

declaration     → classDecl
                | funDecl
                | varDecl
                | statement ;

classDecl       | "class" IDENTIFIER ( "extends" IDENTIFIER )?
                  "{" methodDecl* "}" ;
methodDecl      | "method" function ;
funDecl         | "function" function ;
function        → IDENTIFIER "(" parameters? ")" block ;
parameters      → IDENTITIFIER ( "," IDENTIFIER )* ;
varDecl         → "var" IDENTIFIER ( "=" expression )? ";" ;
               
statement       → exprStmt
                | forStmt
                | ifStmt
                | printStmt
                | returnStmt
                | whileStmt 
                | block ;

exprStmt        → expression ";" ;
forStmt         → "for" "(" ( varDecl | exprStmt | ";" )
                  expression? ";"
                  expression? ")" statement ;
ifStmt          → "if" "(" expression ")" statement 
                  ( "else" statement )? ;
printStmt       → "print" expression ";" ;
returnStmt      → "return" expression? ";" ;
whileStmt       → "while" "(" expression ")" statement ;
block           → "{" declaration* "}" ;

// Expression
expression      → assignment ;
assignment      → ( call "." )? IDENTIFIER "=" assignment
                | logic_or ;
logic_or        → logic_and ( "||" logic_and )* ;
logic_or        → equality ( "&&" equality )* ;
equality        → comparison ( ( "!=" | "==" ) comparison )* ;
comparison      → term ( ( ">" | ">=" | "<" | "<=" ) term )* ;
term            → factor ( ( "-" | "+" ) factor )* ;
factor          → unary ( ( "/" | "*" ) unary )* ;
unary           → ( "-" | "!" ) unary 
                | call ;
call            → primary ( "(" arguments? ")" | "." IDENTIFIER )* ;
arguments       → expression ( "," expression )* ;
primary         → "true" | "false" | "nil"
                | NUMBER | STRING 
                | "this"
                | IDENTIFIER 
                | "(" expression ")" 
                | "super" "." IDENTIFIER ;

Appendix: Clark Virtual Machine

This appendix describes the Clark byte-engine's virtual machine.

NOTE: this information is for the benefit of the language maintainer. The VM's architecture and instruction set details are not part of the language specification, and can change without notice.

The byte-engine is a stack machine with a few registers:

  • ip is the instruction pointer within the current function.
  • T is the temporary register, used to stash a value momentarily during an atomic operation. The Compiler must not compile an arbitrary expression between TSET and TGET, as it might step on the stashed value.

Instruction Set

Mnemonic/argumentStack effectDescription
ADDa bcc = a + b
ASSERTmsg → ∅Throws AssertError
CALL argcf argscc = f(args)
CLASS name∅ → typeCreate class
CLOSURE func...∅ → fLoad closure
COMMENT name∅ → ∅No-op comment
CONST constant∅ → aLoad constant
DECRabb = a - 1
DIVa bcc = a/b
DUPaa aDuplicate top
DUP2a ba b a bDuplicate top 2
EQa bcc = a == b
FALSE∅ → falseLoad false
GEa bcc = a >= b
GETNEXTiteraa = iter.next()
GLOBINDp tflagBind target to pattern
GLOBINOp t → ∅Bind target to pattern
GLODEF namea → ∅Define global
GLOGET name∅ → aGet global
GLOSET nameaaSet global
GTa ba > bCompare: greater
HASNEXTiterflagflag = iter.hasNext()
INa collflaga in collection
INCRabb = a + 1
INDGETcoll iaa = coll[i]
INDSETcoll i aacoll[i] = a
INHERITsup subsupInheritance
ITERcolliteriter = coll.iterator()
JIF offsetflag → ∅Jump if false
JIFKEEP offsetflagflagJump if false, keep value
JIT offsetflag → ∅Jump if true
JITKEEP offsetflagflagJump if true, keep value
JUMP offset∅ → ∅Jump forwards
LEa ba <= bCompare: less or equal
LISTADDlist alistAdd item to list
LISTNEW∅ → listPush empty list
LOCBINDp tvsBind target to pattern
LOCGET slot∅ → aGet local
LOCMOVE slot n... varsvars ...Get local
LOCSET slotaaSet local
LOOP offset∅ → ∅Jump backwards
LTa ba <= bCompare: less than
MAPNEW∅ → mapPush empty ma p
MAPPUTmap k amapAdd entry to map
MATCHp tvs? flagMatch pattern to target
METHOD nametype ftypeAdd method to type
MULa bcc = a*b
NEa bcc = a != b
NEGATEabb = -a
NIa collflaga not in collection
NOTabb = !a
NULL∅ → nullLoad null
PATTERN p bsconstantspvEvaluate the pattern
POPa → ∅Pops one value
POPN na... → ∅Pops n values
PROPGET nameobjaGet property value
PROPSET nameobj aaSet property value
RECORD name fields∅ → typeCreate record type
RETURNaaReturn
RULESET rulesetexportsrsvEvaluate rule set
SUBa bcc = a - b
SUPGET nameobj supfGet superclass method
SWAPa bb aSwap top stack items
TGET∅ → aa = T
THROWa → ∅Throw error
TPUTa → ∅T = a, pop a
TRCPOP∅ → ∅Pops a post-trace
TRCPUSH trace∅ → ∅Pushes a post-trace
TRUE∅ → trueLoad true
TSETaaT = a
UPCLOSE nv... → ∅Closes n upvalue(s)
UPGET slot∅ → aGet upvalue
UPSET slotaaSet upvalue

Stack Effects: in the stack effect column, the top of the stack is on the right.

For SUB, for example, the stack effect is (a bc) where c is (a - b). When the instruction executes, b is on the top of the stack, with a just below it.

Variable Names: In the above table (and in the descriptions below), the italicized names are used as follows:

  • ∅: Indicates that the instruction expects nothing on the stack or leaves nothing on the stack, depending on its position.
  • a, b, c: Arbitrary values
  • argc: An argument count
  • args: A callable's arguments; 0 or more, depending on argc
  • coll: An iterable collection: a Java Collection<?> or a value with a ProxyType<T> that supports iteration
  • constant: An index into the chunk's constants table for an arbitrary constant.
  • f: A callable, native or scripted
  • flag: A boolean flag, true or false.
  • func: A scripted function definition: the Function itself plus its upvalue data, suitable for building into a Closure in the current scope. Used only by the CLOSURE instruction.
  • iter: A collection iterator, as used to compile foreach loops
  • k: A value used as a map key.
  • list: A list value
  • msg: A message string
  • n: A count
  • name: An index into the chunk's constants table for a name constant
  • obj: An object, e.g., a class instance, or any JoeObject
  • offset: A jump offset
  • slot: A stack slot
  • sub: A subclass
  • sup: A superclass
  • trace: A callable trace added to the current call-frame.
  • type: A type
  • v: A local variable

Instructions

ADD


ADD | a bc, where c = (a + b)

  • If a and b are both numbers, yields the sum of the two numbers.
  • If either a or b is a string, converts the other argument to a string and yields the concatenated string.
  • Any other case is reported as an error.

ASSERT


ASSERT | msg → ∅

Throws an AssertError with the given msg. The actual assertion test is compiled using multiple instructions.

CALL


CALL argc | f argsc

Calls callable f with argc arguments; the arguments are found in consecutive stack slots just above f. The result c replaces the function ands its arguments on the stack. Callable f may be any valid native or Bert callable.

CLASS


CLASS name | ∅ → type

The CLASS instruction begins the process of creating a new class from a class declaration. Given name, the index of the class's name in the constants table, it creates a new class type, assigns it to variable name in the current scope, and pushes it onto the stack. Subsequent instructions will add methods, execute static initializers, etc.

CLOSURE


CLOSURE func... | ∅ → f

Converts a func... function definition into Closure f in the current scope. The func argument is a complex set of codes, consisting of:

  • The index of the Function definition in the constants table.
    • The Function knows the number of upvalues.
  • For each Upvalue in the Function:
    • isLocal: 1 if the Upvalue is local to the immediately enclosing scope, and 0 if it is from an outer scope.
    • index: The Upvalue's local variable index

See Compiler::function and VirtualMachine::run/CLOSURE for details.

COMMENT


COMMENT name | ∅ → ∅

A no-op instruction used to insert comments into disassembly listings and execution traces. The name is an index of a string in the constants table; the string serves as the text of the comment.

CONST


CONST constant | ∅ → a

Retrieves the constant with index constant from the constants table, and pushes it onto the stack.

DECR


DECR | ab, where b = (a - 1)

Decrements value a. This is used to implement the -- operator.

DECR is used instead of SUB because it provides a ---specific error message if a is not numeric. SUB's error message would be confusing.

DIV


DIV | a bc, where c = (a/b)

Computes the quotient of a and b.

DUP


DUP | aa a

Duplicates the value on the top of the stack.

DUP2


DUP2 | a ba b a b

Duplicates the top two values on the stack.

EQ


EQ | a bc, where c = (a == b).

Given two values a and b, yields true if the two are equal, and false otherwise.

FALSE


FALSE | ∅ → false

Pushes false onto the stack.

GE


GE | a bc, where c = (a >= b)

Yields true if a is greater than or equal to b, and false otherwise.

GETNEXT


GETNEXT | itera

Gets the next value from iter, which must be an iterator created by the ITER instruction. This instruction should always be paired with HASNEXT.

GLOBIND


GLOBIND | p tflag

The GLOBIND instruction matches pattern p against target t, binding pattern variables to matched values and pushing true on success and binding pattern variables to null and pushing false on failure, where p is a pattern evaluated by the PATTERN instruction and t is any Joe value.

GLOBINO


GLOBINO | p t → ∅

Legacy: The GLOBINO instruction implements the var statement at the global scope when var includes a destructuring pattern, binding pattern value p to target t, where p is a pattern evaluated by the PATTERN instruction. If the match succeeds then the binding variables and their values will be added to the global environment. If the match fails then the instruction throws a RuntimeError.

GLODEF


GLODEF name | a → ∅

Defines a global variable called name, assigning it the value a.

GLOGET


GLOGET name | ∅ → a

Retrieves the value of global variable name and pushes it on the stack.

GLOSET


GLOSET name | aa

Assigns value a to global variable name, leaving a on the stack.

GT


GT | a bc, where c = (a > b)

Yields true if a is greater than or equal to b, and false otherwise.

HASNEXT


HASNEXT | iterflag

Gets whether iterator iter has another value. The iter must be an iterator created by the ITER instruction.

IN


IN | a collflag

Pushes true if value a is in collection coll, and false otherwise. The coll can be any Java Collection<?> or a value whose ProxyType<T> makes it iterable.

INCR


INCR | ab, where b = (a + 1)

Increments value a. INCR is used to implement the ++ operator.

INCR is used instead of ADD for two reasons:

  • INCR provides a ++-specific error message if a is not numeric. ADD's error message would be confusing.
  • ADD will concatenate strings, but ++ is only for use with numbers.

INDGET


INDGET | coll ia

Retrieves the value at index i in the indexed collection coll, and pushes it on the stack.

  • If coll is a Joe List, i must be an index to an existing item.
  • If coll is a Joe Map, i can be any Monica value; returns null if the value doesn't exist.
  • It is a runtime error if coll is neither a List nor a Map.

INDSET


INDSET | coll i aa

Assigns the value a to index i in the indexed collection coll, leaving a on the stack.

  • If coll is a Joe List, i must be an index to an existing item.
  • If coll is a Joe Map, i can be any Monica value.
  • It is a runtime error if coll is neither a List nor a Map.

INHERIT


INHERIT | sup subsup

This instruction is used when compiling a class definition. Given superclass sup and the class being defined, sub, copies all of sup's methods to sub's methods table, thereby inheriting sup's behavior. NOTE: If Joe were modified to allow methods to be added to superclasses, this implementation would need to change.

ITER


ITER | colliter

Give collection coll, creates a Java Iterator<?> for the collection. The coll can be any Java Collection<?> or value whose ProxyType<?> provides iterability. This is used to implement the foreach statement.

JIF


JIF offset | flag → ∅

Jumps forward by offset if flag is false.

JIFKEEP


JIFKEEP offset | flagflag

Jumps forward by offset if flag is false, retaining the flag on the stack.

JIT


JIT offset | flag → ∅

Jumps forward by offset if flag is true.

JITKEEP


JITKEEP offset | flagflag

Jumps forward by offset if flag is true, retaining the flag on the stack.

JUMP


JUMP offset | ∅ → ∅

Jumps forward by offset.

LE


LE | a bc, where c = (a <= b)

LISTADD


LISTADD | list alist

Pops a and adds it to the list value.

LISTNEW


LISTNEW | ∅ → list

Pushes an empty list value onto the stack.

LOCBIND


LOCBIND | p tvs

The LOCBIND instruction implements the var statement in local scopes when var includes a destructuring pattern, binding pattern value p to target value t, where p is a pattern value produced by the PATTERN instruction. If the match succeeds then the values of the binding variables will be pushed onto the stack, initializing them as locals. If the match fails then the instruction throws a RuntimeError.

LOCGET


LOCGET slot | ∅ → a

Gets the value of the local variable in the given stack slot, relative to the current call frame.

LOCMOVE


LOCMOVE slot n | ... varsvars ...

Shifts the top n items on the stack down to the given local variable *slot, moving the displaced items up.

This instruction is used by expressions that declare local variables (e.g., the ~ operator) to move them below any transient values below them on stack.

LOCSET


LOCSET slot | aa

Sets the value of the local variable in the given stack slot, relative to the current call frame, to a, leaving the value on the stack.

LOOP


LOOP offset | ∅ → ∅

Jumps backward by offset.

LT


LT | a bc, where c = (a < b)

MAPNEW


MAPNEW | ∅ → map

Pushes an empty map value onto the stack.

MAPPUT


MAPPUT | map k amap

Pops k and a and puts {k: a} into the map value.

MATCH


MATCH | p tvs? flag

Matches pattern p against the target value t. On success pushes the values of the pattern's bound variables followed by true; on failure pushes false.

This is used for foreach and match, where we don't want to bind the variables on failure. These statements define local scopes, so MATCH is never used at global scope.

MATCHG


MATCHG | p tflag

Matches pattern p against the target value t. On success saves the pattern's bound variables to the global environment and pushes true; on failure pushes saves nulls and pushes false.

This is used for the ~ operator at global scope, where the variables must be declared in the enclosing scope whether the match succeeds or not.

MATCHL


MATCHL | p tvs? flag

Matches pattern p against the target value t. On success pushes the values of the pattern's bound variables followed by true; on failure pushes nulls followed by false.

This is used for the ~ at local scope, where the variables must be declared in the enclosing scope whether the match succeeds or not.

METHOD


METHOD name | type ftype

This instruction is used when compiling a type definition. Adds closure f to type as a method. It is illegal to add a method to a type after its definition.

MUL


MUL | a b → *c, where c = (a * b)

Computes the product of a and b.

NE


NE | a bc, where c = (a != b)

Pushes true if a and b are not equal, and false otherwise.

NEGATE


NEGATE | ab, where b = (-a)

Negates a.

NI


NI | a collflag

Pushes false if value a is in collection coll, and true otherwise. The coll can be any Java Collection<?> or a value whose ProxyType<T> makes it iterable.

NOT


NOT | ab, where b = (!a)

NULL


NULL | ∅ → null

Pushes null on the stack.

PATTERN


PATTERN p bs | constantspv

Given p, a Pattern, and bs, a List<String> of binding variable names, and a list of evaluated pattern constants, produces a PatternValue for use with GLOBIND, LOCBIND, and MATCH.

The PatternValue is used internally only; it is never exposed to the Joe client.

POP


POP | a -> ∅

Pops one value from the stack.

POPN


POPN n | a... → ∅

Pops n values from the stack.

PROPGET


PROPGET name | obja

Gets the value of property name of object obj.

PROPSET


PROPSET name | obj aa

Sets property name of object obj to a, leaving a on the stack.

RECORD


RECORD name fields | ∅ → type

The RECORD instruction begins the process of creating a new type from a record declaration. Given name, the index of the type's name in the constants table, and fields, the index of the type's list of field names in the constants table, it creates a new type, assigns it to variable name in the current scope, and pushes it onto the stack. Subsequent instructions will add methods, execute static initializers, etc.

RETURN


RETURN | aa

Returns a from the current function/call frame. If the function was invoked from Java via VirtualMachine::execute or VirtualMachine::callFromJava, the result is popped and returned to the Java caller.

RULESET


RULESET ruleset | exportsrsv

Given a RuleSet as a constant, and a map of callable expressions by relation name on the stack, pushes a RuleSetValue.

SUB


SUB | a b → *c, where c = (a - b)

Computes the difference of a and b.

SUPGET


SUPGET name | obj supf

Retrieves method name for superclass sup of object obj. This is used to compile the super.<method> syntax.

SWAP


SWAP | a bb a

Swaps the top two items on the stack.

TGET


TGET | ∅ → a

Pushes the value of the T register onto the stack.

THROW


THROW | a -> ∅

Throws a MonicaError given a, which may be a MonicaError or a string, creating a new MonicaError in the latter case.

TPUT


TPUT | a → ∅

Pops and loads a into the T register.

TRCPOP


TRCPOP | ∅ → ∅

Pops a post-trace from the current CallFrame. See TRCPUSH.

TRCPUSH


TRCPUSH trace | ∅ → ∅

Pushes a post-trace into the current CallFrame. The trace is a Trace constant identifying a particular scope in a loaded script. If an error is thrown while the post-trace is present in the CallFrame, the trace will be included in the error stack trace.

This is used to add the class as a stack level when errors are found in a class static initializer block.

TSET


TSET | aa

Loads a into the T register, leaving it on the stack.

TRUE


TRUE | ∅ → true

Pushes true onto the stack.

UPCLOSE


UPCLOSE n | v... → ∅

Pops n local variables from the stack, closing any of them that are open upvalues. This is used when ending a scope, and by break and continue when ending a scope prematurely.

UPGET


UPGET slot | ∅ → a

Gets the value of the local variable in the given stack slot, relative to the current call frame. This is used when the local variable has been captured by a closure.

UPSET


UPSET slot | aa

Sets the value of the local variable in the given stack slot, relative to the current call frame, to a, leaving the value on the stack. This is used when the local variable has been captured by a closure.