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
Point is, I've got reasons; here they are; I'm not inclined to argue about them.
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.
- 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.
- 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.
- Nero can be used as a standalone language via the
- Merged
let
's capability intovar
, and removedlet
.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 existingif 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.
- Added Nero, an implementation of the Datalog query language.
- Extensibility
- Simplified implementation of proxy types for native record types.
- Any
ProxyType
can now easily define read-only fields for its proxied values.
- Any
- Simplified implementation of proxy types for native record types.
- 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.
- Revised
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
andset
methods, and for theMap
get
andput
methods.
- Array notation is sugar for the
- 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.
- Lists may now be created with list literal syntax, e.g.,
- 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 SeeCatchResult
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.
- The infrastructure can also be used with non-
- 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 ofJoe
:Joe.compare()
Joe.currentTimeMillis()
(replacing the globalmillis()
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 theClass::getName
value for the Java type.
- Values of opaque types are now provided with an ad hoc proxy that
provides a
- Added the
CatchResult
type as the result of thecatch()
method, replacing theTuple
type. - Deleted the
Tuple
type, as it now seems ill-conceived.
- Added the
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
- A default
-
Bert Byte-code Engine
- Added
BertEngine
, a byte-code execution engine, following Nystrom'sclox
design in Java. BertEngine
andWalkerEngine
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 relevantjoe
tools.
- Added
-
Extending/Embedding API
- Added
Joe::isComplete
. - It is now possible to create bindings for native types that can be extended by scripted classes.
- Added
-
Library
- Experimental
joe.win
Package- Added JavaFX widgets:
Menu
,MenuBar
,MenuItem
,Separator
,Tab
,TabPane
,ListView
,SplitPane
,GridPane
- Added JavaFX enums:
Orientation
,Side
- Added JavaFX widgets:
- Experimental
-
Bugs fixed
- Test script
print
/println
output is now hidden unlessjoe test
is run with the--verbose
option.
- Test script
Changes in 0.4.0
-
Language
- Added the
@
operator.- In class methods,
@name
is identical tothis.name
.
- In class methods,
- Added the
-
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.
- Instances of
- Moved the arity checking methods (e.g.,
Joe.exactArity()
) fromJoe
toArgs
.
- It is now possible to define native types that more fully resemble
Joe classes and instances, e.g., the new
-
Library
- Added experimental
joe.win
package for creating JavaFX GUIs in Joe.- Optional package.
- Loaded (with
joe.console
) by newjoe 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 aTuple
rather than aPair
. - Removed
Pair
.
- The standard
- Replaced the
StringBuilder
type with the newTextBuilder
type, which is a native type that can be subclassed by Joe classes.
- Added experimental
-
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
- Added
joe test
- Added
assertTrue
,assertFalse
,assertError
, andskip
functions.
- Added
joe version
- New tool, outputs the Joe version and build date.
- Experimental
-
Miscellaneous
- Removed
Joe::codify
. - Removed the various flavors of
Joe::recodify
. - Added Ant
build.xml
for building releases.
- Removed
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.
- Can be entered using scientific notation, e.g.,
-
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
andni
membership operators. - Added the
+=
,-=
,*=
, and/=
assignment operators, with their usual semantics. - Added the
++
and--
operators, with their usual semantics.
- Added the
-
Statements
- Added the
switch
statement.
- Added the
-
-
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 theError
type. - Added the
StringBuilder
type. - Added the optional
joe.console
package, for use by scripts invoked by the command line, including thePath
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 theStringBuilder
type.
-
joe run
Tool- Installs the
joe.console
package.
- Installs the
-
joe repl
Tool- Installs the
joe.console
package.
- Installs the
-
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.
- A
-
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.
- Fixed broken
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 withprintln()
, 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 includestatic method
declarations andstatic {...}
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
andcontinue
statements. - Added
foreach
statement. JoeError
exceptions now include a script-level stack trace, populated as the stack is unwound.
- Added
-
Library
- Added
catch()
function - Added
compare()
function - Added
println()
,print()
functions - Added
Error
proxy (forJoeError
and its subclasses) - Added
Keyword
Java type, with proxy - Added
List
Java type, with proxy - Added
Number
proxy (all numbers are JavaDoubles
) - Added
Pair
Java type, with proxy - Added
String
proxy
- Added
-
Embedding API
- Cleaned up top-level error handling
- Added
Joe::installScriptResource
- Added
TypeProxy<V>
, a class that defines a binding for native Java typeV
. ATypeProxy
can provide- Instance methods and an initializer for type V
- E.g., a
String
'slength()
method.
- E.g., a
- Static methods and constants
- E.g.,
Double.PI
,Double.abs()
.
- E.g.,
- Instance methods and an initializer for type V
- 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 toSystem.out
.- The methods
Joe::println
andJoe::print
can be used from Java bindings.
- The methods
-
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 ofjoe doc
output.)
- Added
-
Development
- Added
joe/tests
, the script-level Joe test suite.
- Added
-
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
replacesnil
.&&
replacesand
.||
replacesor
.- Strings can include the standard string escapes, which are converted into the usual characters.
- '\\',
\t
,\b
,\n
,\r
,\f
,\"
- '\\',
function
replacesfun
as the function definition keyword.method
is added as the method definition keyword.- The
extends
keyword replaces<
inclass 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.
- The
- Embedding API
- The
Joe
engine is separated from theApp
application, so that a client can createJoe
instances as needed.- The embedding API is still mostly non-existent.
- The
- Tools
- The
App
includes "tool" infrastructure, and supports two tools, accessed asjoe run
andjoe 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
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:
null
- Booleans
- Numbers
- Strings
- Raw String Literals
- Keywords
- Lists
- Maps
- Sets
- Errors
- Functions and Methods
- Classes
- Records
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.
Joe supports Unicode escapes, e.g., \u1234
, as in Java;
but only within string literals.
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
- Function Declarations
- Class Declarations
- Expression Statements
- Blocks
- Return
- If Statements
- While Loops
- For Loops
- Foreach Loops
- Break and Continue
- Switch Statements
- Match Statements
- Throw
- Assert
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 thecase
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
- Binding Variables
- Wildcards
- Constants
- Interpolated Expressions
- List Patterns
- Map Patterns
- Matching Instances with Map Patterns
- Named-Field Patterns
- Ordered-Field Patterns
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 variableString
.
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
, ornull
.
- A Joe keyword, string, number,
- A literal constant term in a Nero program must be one of the following:
- The string
Parent/2
appearing in the comment indicates thatParent
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
andy
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 termsx
andz
are bound to the parent and child in that fact. - Then, some
Ancestor
fact is found in which individualz
is the ancestor;y
is then bound to the descendant. - The bound values of
x
andy
are then inserted intoAncestor(x, y)
to produce the newAncestor
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
andtoFact
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 theAncestor
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
, ornull
.
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
, andnull
. - In scripted input facts: any Joe value
- In rules and axioms: keywords, strings, numbers,
- 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 haveC(#anne)
and noB(#anne)
B(#anne)
because we haveA(#anne)
- But now we have both
A(#anne)
andB(#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 relationB
with negation, directly or indirectly, relationB
cannot depend on relationA
, 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
- Datalog (wikipedia)
- Datalog and Logic Databases by Greco & Molinaro
- Modern Datalog Engines by Ketsman & Koutris
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
- Installing Bindings
- Executing Scripts
- Executing a REPL
- Redirecting Output
- Practical Usage
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 typeinstallScriptResource()
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 theline()
method.AssertError
is an error thrown by Joe'sassert
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'sthrow
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
.
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.
- Joe and Java Data Types
- Native Functions
- Registered Types
- Joe Packages
- Native Classes
- Native Records
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 Type | Java Type |
---|---|
Boolean | java.lang.Boolean |
Error | com.wjduquette.joe.JoeError (a RuntimeException ) |
Keyword | com.wjduquette.joe.Keyword |
List | com.wjduquette.joe.JoeValue (a List<Object> ) |
Number | java.lang.Double |
String | java.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.
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 theJoe
interpreter, andargs
, 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 aDouble
and returns it as adouble
, or throws aJoeError
.
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 JavaString
. -
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
- The Proxied Types
- Type Lookup
- The Supertype
- Stringification
- Iterability
- Static Constants
- Static Methods
- Initializer
- Static Types
- Instance Methods
- Installing a Proxy Type
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 JavaDouble
; it's calledNumber
because there's only one kind of number in Joe. - Joe's
List
type actually maps to two differentList<Object>
types, both under the umbrella of theJoeList
interface. Calling it simplyList
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 theproxyTable
, 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 ownClass
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'sClass
.
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.
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
- Test Outcomes
- Test Scaffolding
- Test Checkers
- Running Test Scripts
- Extending the Test Runner
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:
Result | Meaning |
---|---|
Success | The test ran without error |
Failure | The test threw an AssertError |
Error | The 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 Tag | Meaning |
---|---|
@title string | Gives 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 Tag | Meaning |
---|---|
@title string | Gives 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 Tag | Meaning |
---|---|
@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 argumentx
.x, y
: The two argumentsx
andy
.start, [end]
: The two argumentsstart
andend
;end
is optional.x, ...
: The argumentx
plus any number of additional arguments.name, value, [name, value]...
: One or morename
,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 Tag | Meaning |
---|---|
@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 Tag | Meaning |
---|---|
@title string | Gives 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.
JoeDoc Links
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.
Entity | Qualified | Unqualified |
---|---|---|
@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 theString
type'slength
method, from anywhere in the documentation set.[[String#method.length]]
links to theString
type'slength
method, from anywhere in thejoe
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.mdtype.<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
.
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.
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
package (Joe Standard Library)- catch(callable) → CatchResult
- print(text)
- printf(fmt, [values...])
- println([text])
- AssertError extends Error
- Boolean
- CatchResult
- Error
- Error(message, [trace, ...]) → Error
- error.addInfo(message) → this
- error.javaStackTrace() → String
- error.message() → text
- error.stackTrace() → String
- error.traces() → List
- error.type() → name
- Fact
- Function
- Joe
- Keyword
- List
- List.of(items...) → List
- List() → List
- List(other) → List
- List(size, [initValue]) → List
- list.add([index], item) → this
- list.addAll([index], collection) → this
- list.clear() → this
- list.contains(value) → Boolean
- list.containsAll(collection) → Boolean
- list.copy() → List
- list.filter(predicate) → List
- list.get(index) → value
- list.getFirst() → value
- list.getLast() → value
- list.indexOf(value) → Number
- list.isEmpty() → Boolean
- list.lastIndexOf(value) → Number
- list.map(func) → List
- list.peekFirst() → value
- list.peekLast() → value
- list.remove(value) → Boolean
- list.removeAll(collection) → Boolean
- list.removeAt(index) → value
- list.removeFirst() → value
- list.removeLast() → value
- list.reverse() → List
- list.set(index, newValue) → oldValue
- list.size() → Number
- list.sorted([comparator]) → List
- list.sublist(start, [end]) → List
- list.toString() → String
- Map
- Map.of(values...)
- Map([other]) → Map
- map.clear() → this
- map.containsKey(key) → Boolean
- map.containsValue(value) → Boolean
- map.copy() → Map
- map.get(key) → value
- map.getOrDefault(key, defaultValue) → value
- map.isEmpty() → Boolean
- map.keySet() → Set
- map.put(key, value) → value
- map.putAll(map) → this
- map.remove(key) → value
- map.size() → Number
- map.toString() → String
- map.values() → List
- Number
- Number.E
- Number.MAX_INT
- Number.MAX_VALUE
- Number.MIN_INT
- Number.MIN_VALUE
- Number.NAN
- Number.NEGATIVE_INFINITY
- Number.PI
- Number.POSITIVE_INFINITY
- Number.TAU
- Number.abs(num) → Number
- Number.acos(num) → Number
- Number.asin(num) → Number
- Number.atan(num) → Number
- Number.atan2(x, y) → Number
- Number.ceil(num) → Number
- Number.clamp(num, min, max) → Number
- Number.cos(a) → Number
- Number.exp(num) → Number
- Number.floor(num) → Number
- Number.hypot(x, y) → Number
- Number.log(num) → Number
- Number.log10(num) → Number
- Number.max(num...) → Number
- Number.min(num...) → Number
- Number.mod(a, b) → Number
- Number.pow(a, b) → Number
- Number.random() → Number
- Number.round(num) → Number
- Number.sin(a) → Number
- Number.sqrt(num) → Number
- Number.tan(a) → Number
- Number.toDegrees(radians) → Number
- Number.toRadians(degrees) → Number
- Number(string) → Number
- Opaque
- RuleSet
- Set
- Set(values...) → Set
- set.add(value) → Boolean
- set.addAll(collection) → Boolean
- set.clear() → this
- set.contains(value) → Boolean
- set.copy() → Set
- set.filter(predicate) → Set
- set.isEmpty() → Boolean
- set.map(func) → Set
- set.remove(value) → Boolean
- set.removeAll(collection) → Boolean
- set.size() → Number
- set.sorted([comparator]) → List
- set.toString() → String
- String
- String.format(fmt, [values...]) → String
- String.join(delimiter, list) → String
- String(value) → String
- string.charAt(index) → String
- string.contains(target) → Boolean
- string.endsWith(suffix) → Boolean
- string.equalsIgnoreCase(other) → Boolean
- string.indent(n) → String
- string.indexOf(target, [beginIndex], [endIndex]) → Number
- string.isBlank() → Boolean
- string.isEmpty() → Boolean
- string.lastIndexOf(target, [fromIndex]) → Number
- string.length() → Double
- string.lines() → List
- string.matches(pattern) → Boolean
- string.repeat(count) → String
- string.replace(target, replacement) → String
- string.replaceAll(regex, replacement) → String
- string.replaceFirst(regex, replacement) → String
- string.split(delimiter) → List
- string.splitWithDelimiters(delimiter) → List
- string.startsWith(prefix) → Boolean
- string.strip() → String
- string.stripIndent() → String
- string.stripLeading() → String
- string.stripTrailing() → String
- string.substring(beginIndex, [endIndex]) → String
- string.toLowerCase() → String
- string.toString() → String
- string.toUpperCase() → String
- TextBuilder
- Type
joe.console
package- Console
- Path
- Path.compare(a, b) → Number
- Path(first,[more...]) → Path
- path.endsWith(path) → Boolean
- path.getFileName() → Path
- path.getName(index) → Path
- path.getNameCount() → Number
- path.getParent() → Path
- path.isAbsolute() → Boolean
- path.normalize() → Path
- path.relativize(other) → Path
- path.resolve(other) → Path
- path.startsWith(path) → Boolean
- path.subpath(start, [end]) → Path
- path.toAbsolutePath() → Path
- path.toString() → String
joe.doc
package (JoeDoc Configuration API)joe.test
package (Joe Test Tool API)- assertEquals(got, expected)
- assertError(callable, [message], [frames...])
- assertFalse(condition)
- assertTrue(condition)
- check(value) → ValueChecker
- checkCatch(callable) → CatchChecker
- engine() → String
- fail(message)
- skip(message)
- CatchChecker
- CatchChecker(catchResult) → CatchChecker
- catchChecker.isError() → Boolean
- catchChecker.isOK() → this
- catchChecker.message(expected) → this
- catchChecker.stackFrames(frame,...) → this
- catchChecker.stackTrace(expected) → this
- catchChecker.type(expected) → this
- JoeTest
- ValueChecker
- ValueChecker(value) → ValueChecker
- valueChecker.containsAll(values...) → this
- valueChecker.eq(expected) → this
- valueChecker.hasType(type) → this
- valueChecker.hasTypeName(name) → this
- valueChecker.isEmpty() → this
- valueChecker.isFalse() → this
- valueChecker.isNotNull() → this
- valueChecker.isNull() → this
- valueChecker.isTrue() → this
joe.win
package- Button extends Control
- Button([text], [action]) → Button
- button.action(callable) → this
- button.text(text) → this
- Control extends Region
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
- GridPane extends Pane
- HBox extends Pane
- HPos
- HPos.CENTER
- HPos.LEFT
- HPos.RIGHT
- HPos.values() → List
- HPos(value) → HPos
- hPos.name() → String
- hPos.ordinal() → Number
- hPos.toString() → String
- Insets
- Insets(pixels) → Insets
- Insets(top, right, bottom, left) → Insets
- insets.getBottom() → Number
- insets.getLeft() → Number
- insets.getRight() → Number
- insets.getTop() → Number
- Label extends Control
- Label([text]) → Label
- label.text(text) → this
- ListView extends Control
- ListView() → ListView
- listView.getItems() → joe.List
- listView.getSelectedIndex() → joe.Number
- listView.getSelectedItem() → item
- listView.item(item) → this
- listView.items(list) → this
- listView.onSelect(callable) → this
- listView.placeholder(node) → this
- listView.placeholderText(text) → this
- listView.selectIndex(index) → this
- listView.selectItem(item) → this
- listView.stringifier(callable) → this
- Menu
- Menu() → Menu
- menu.disable([flag]) → this
- menu.getProperty(keyword) → value
- menu.id(id) → this
- menu.isDisabled() → joe.Boolean
- menu.item(item) → this
- menu.items() → joe.List
- menu.listenTo(keyword, listener) → this
- menu.properties() → joe.Set
- menu.setProperty(keyword, value) → this
- menu.styleClasses() → joe.List
- menu.styles(style, ...) → this
- menu.text(text) → this
- menu.toString() → joe.String
- MenuBar extends Control
- MenuItem
- MenuItem() → MenuItem
- menuItem.action(callable) → this
- menuItem.disable([flag]) → this
- menuItem.getProperty(keyword) → value
- menuItem.id(id) → this
- menuItem.isDisabled() → joe.Boolean
- menuItem.listenTo(keyword, listener) → this
- menuItem.properties() → joe.Set
- menuItem.setProperty(keyword, value) → this
- menuItem.styleClasses() → joe.List
- menuItem.styles(style, ...) → this
- menuItem.text(text) → this
- menuItem.toString() → joe.String
- Node
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
- Orientation
- Orientation.HORIZONTAL
- Orientation.VERTICAL
- Orientation.values() → List
- Orientation(value) → Orientation
- orientation.name() → String
- orientation.ordinal() → Number
- orientation.toString() → String
- Pane extends Region
- Pane() → Pane
- pane.child(node) → this
- pane.children() → joe.List
- Pos
- Priority
- Priority.ALWAYS
- Priority.NEVER
- Priority.SOMETIMES
- Priority.values() → List
- Priority(value) → Priority
- priority.name() → String
- priority.ordinal() → Number
- priority.toString() → String
- Region extends Node
- Region.USE_COMPUTED_SIZE
- Region.USE_PREF_SIZE
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
- Separator extends Control
- Separator() → Separator
- separator.horizontal() → this
- separator.vertical() → this
- Side
- Side.BOTTOM
- Side.LEFT
- Side.RIGHT
- Side.TOP
- Side.values() → List
- Side(value) → Side
- side.name() → String
- side.ordinal() → Number
- side.toString() → String
- SplitPane extends Control
- StackPane extends Pane
- Tab
- Tab() → Tab
- tab.content(node) → this
- tab.disable([flag]) → this
- tab.getProperty(keyword) → value
- tab.id(id) → this
- tab.isDisabled() → joe.Boolean
- tab.isSelected() → joe.Boolean
- tab.listenTo(keyword, listener) → this
- tab.properties() → joe.Set
- tab.setProperty(keyword, value) → this
- tab.styleClasses() → joe.List
- tab.styles(style, ...) → this
- tab.tabPane() → TabPane
- tab.text(text) → this
- tab.toString() → joe.String
- TabPane extends Control
- TabPane([text]) → TabPane
- tabPane.tab(tab) → this
- tabPane.tabs() → joe.List
- VBox extends Pane
- VPos
- VPos.BASELINE
- VPos.BOTTOM
- VPos.CENTER
- VPos.TOP
- VPos.values() → List
- VPos(value) → VPos
- vPos.name() → String
- vPos.ordinal() → Number
- vPos.toString() → String
- Win
- Win.css(css) → this
- Win.cssFile(filename) → this
- Win.root() → VBox
- Win.setSize(width, height) → this
- Win.setTitle(title) → this
- Button extends Control
Joe Standard Library (joe
)
The joe
package contains Joe's standard library.
Types
- AssertError extends Error
- Boolean
- CatchResult
- Error
- Fact
- Function
- Joe
- Keyword
- List
- Map
- Number
- Opaque
- RuleSet
- Set
- String
- TextBuilder
- Type
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.
Error Methods
- error.addInfo(message) → this
- error.javaStackTrace() → String
- error.message() → text
- error.stackTrace() → String
- error.traces() → List
- error.type() → name
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
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
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.
- error.addInfo(message) → this
- error.javaStackTrace() → String
- error.message() → text
- error.stackTrace() → String
- error.traces() → List
- error.type() → name
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
.
- fact.fieldMap() → Map
- fact.fields() → List
- fact.isOrdered() → Boolean
- fact.relation() → String
- fact.toString() → String
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.
- Joe.compare(a, b) → Number
- Joe.currentTimeMillis() → Number
- Joe.getFieldNames(value) → List
- Joe.isFact(value) → Boolean
- Joe.isOpaque(value) → Boolean
- Joe.isType(value) → Boolean
- Joe.stringify(value) → String
- Joe.supertypeOf(type) → type
- Joe.toFact(value) → Fact
- Joe.typeOf(value) → type
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.
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.
- list.add([index], item) → this
- list.addAll([index], collection) → this
- list.clear() → this
- list.contains(value) → Boolean
- list.containsAll(collection) → Boolean
- list.copy() → List
- list.filter(predicate) → List
- list.get(index) → value
- list.getFirst() → value
- list.getLast() → value
- list.indexOf(value) → Number
- list.isEmpty() → Boolean
- list.lastIndexOf(value) → Number
- list.map(func) → List
- list.peekFirst() → value
- list.peekLast() → value
- list.remove(value) → Boolean
- list.removeAll(collection) → Boolean
- list.removeAt(index) → value
- list.removeFirst() → value
- list.removeLast() → value
- list.reverse() → List
- list.set(index, newValue) → oldValue
- list.size() → Number
- list.sorted([comparator]) → List
- list.sublist(start, [end]) → List
- list.toString() → String
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.
- map.clear() → this
- map.containsKey(key) → Boolean
- map.containsValue(value) → Boolean
- map.copy() → Map
- map.get(key) → value
- map.getOrDefault(key, defaultValue) → value
- map.isEmpty() → Boolean
- map.keySet() → Set
- map.put(key, value) → value
- map.putAll(map) → this
- map.remove(key) → value
- map.size() → Number
- map.toString() → String
- map.values() → List
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.
- Number.E
- Number.MAX_INT
- Number.MAX_VALUE
- Number.MIN_INT
- Number.MIN_VALUE
- Number.NAN
- Number.NEGATIVE_INFINITY
- Number.PI
- Number.POSITIVE_INFINITY
- Number.TAU
- Number.abs(num) → Number
- Number.acos(num) → Number
- Number.asin(num) → Number
- Number.atan(num) → Number
- Number.atan2(x, y) → Number
- Number.ceil(num) → Number
- Number.clamp(num, min, max) → Number
- Number.cos(a) → Number
- Number.exp(num) → Number
- Number.floor(num) → Number
- Number.hypot(x, y) → Number
- Number.log(num) → Number
- Number.log10(num) → Number
- Number.max(num...) → Number
- Number.min(num...) → Number
- Number.mod(a, b) → Number
- Number.pow(a, b) → Number
- Number.random() → Number
- Number.round(num) → Number
- Number.sin(a) → Number
- Number.sqrt(num) → Number
- Number.tan(a) → Number
- Number.toDegrees(radians) → Number
- Number.toRadians(degrees) → Number
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
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.
- ruleSet.infer([inputs]) → Set
- ruleSet.isDebug() → Boolean
- ruleSet.isStratified() → Boolean
- ruleSet.setDebug(flag)
- ruleSet.toString() → String
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.
- set.add(value) → Boolean
- set.addAll(collection) → Boolean
- set.clear() → this
- set.contains(value) → Boolean
- set.copy() → Set
- set.filter(predicate) → Set
- set.isEmpty() → Boolean
- set.map(func) → Set
- set.remove(value) → Boolean
- set.removeAll(collection) → Boolean
- set.size() → Number
- set.sorted([comparator]) → List
- set.toString() → String
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.
- string.charAt(index) → String
- string.contains(target) → Boolean
- string.endsWith(suffix) → Boolean
- string.equalsIgnoreCase(other) → Boolean
- string.indent(n) → String
- string.indexOf(target, [beginIndex], [endIndex]) → Number
- string.isBlank() → Boolean
- string.isEmpty() → Boolean
- string.lastIndexOf(target, [fromIndex]) → Number
- string.length() → Double
- string.lines() → List
- string.matches(pattern) → Boolean
- string.repeat(count) → String
- string.replace(target, replacement) → String
- string.replaceAll(regex, replacement) → String
- string.replaceFirst(regex, replacement) → String
- string.split(delimiter) → List
- string.splitWithDelimiters(delimiter) → List
- string.startsWith(prefix) → Boolean
- string.strip() → String
- string.stripIndent() → String
- string.stripLeading() → String
- string.stripTrailing() → String
- string.substring(beginIndex, [endIndex]) → String
- string.toLowerCase() → String
- string.toString() → String
- string.toUpperCase() → String
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:
Conversion | Input Type | Description |
---|---|---|
b , B | Any | true if truthy, false if falsey |
h , H | Any | The value's hash code as a hex string. |
s , S | Any | The value, stringified. |
d | Number | Converted to integer and formatted. |
x , X | Number | Converted to integer, formatted as hex |
e , E | Number | Decimal, scientific notation |
f | Number | Decimal |
g , G | Number | Decimal or scientific notation, as appropriate. |
% | n/a | A literal percent sign |
n | n/a | The 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
, andX
conversions.
Flags
The supported flags are as follows, and apply only to the specified argument type.
Flag | Input Type | Description |
---|---|---|
- | Any | Result is left-justified. |
+ | Number | Result always includes a sign |
space | Number | Leading space for positive values |
0 | Number | Padded with leading zeros |
, | Number | Use local-specific grouping separators |
( | Number | Enclose 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.
- textBuilder.append(value) → this
- textBuilder.clear() → this
- textBuilder.print(value) → this
- textBuilder.printf(fmt, [values...])
- textBuilder.println(value) → this
- textBuilder.toString() → String
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.
- assertEquals(got, expected)
- assertError(callable, [message], [frames...])
- assertFalse(condition)
- assertTrue(condition)
- check(value) → ValueChecker
- checkCatch(callable) → CatchChecker
- engine() → String
- fail(message)
- skip(message)
Types
Imported Types
In addition to the APIs documented here, joe.test
includes types from
other packages so that they can be tested:
- From
joe.console
,Path
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.
- catchChecker.isError() → Boolean
- catchChecker.isOK() → this
- catchChecker.message(expected) → this
- catchChecker.stackFrames(frame,...) → this
- catchChecker.stackTrace(expected) → this
- catchChecker.type(expected) → this
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
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.
- valueChecker.containsAll(values...) → this
- valueChecker.eq(expected) → this
- valueChecker.hasType(type) → this
- valueChecker.hasTypeName(name) → this
- valueChecker.isEmpty() → this
- valueChecker.isFalse() → this
- valueChecker.isNotNull() → this
- valueChecker.isNull() → this
- valueChecker.isTrue() → this
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.
- DocConfig.inputFile(filename,...) → this
- DocConfig.inputFolder(folder,...) → this
- DocConfig.outputFolder(folder) → this
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.
- Console.args() → List
- Console.cd(path) → Path
- Console.exit([code])
- Console.mkdir(path)
- Console.pwd() → Path
- Console.read() → String
- Console.readFile(filePath) → String
- Console.readLines(filePath) → List
- Console.writeFile(filePath, text)
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.
- Path.compare(a, b) → Number
- path.endsWith(path) → Boolean
- path.getFileName() → Path
- path.getName(index) → Path
- path.getNameCount() → Number
- path.getParent() → Path
- path.isAbsolute() → Boolean
- path.normalize() → Path
- path.relativize(other) → Path
- path.resolve(other) → Path
- path.startsWith(path) → Boolean
- path.subpath(start, [end]) → Path
- path.toAbsolutePath() → Path
- path.toString() → String
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.
Types
- Button extends Control
- Control extends Region
- GridPane extends Pane
- HBox extends Pane
- HPos
- Insets
- Label extends Control
- ListView extends Control
- Menu
- MenuBar extends Control
- MenuItem
- Node
- Orientation
- Pane extends Region
- Pos
- Priority
- Region extends Node
- Separator extends Control
- Side
- SplitPane extends Control
- StackPane extends Pane
- Tab
- TabPane extends Control
- VBox extends Pane
- VPos
- Win
Widget Hierarchy
The joe.win
widget type hierarchy is a subset of the JavaFX hierarchy.
MenuItem
: A menu itemMenu
: A menuNode
: Base classRegion
: Nodes with geometry
Tab
: A tab in aTabPane
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 HTMLstyle
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.
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
Control
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#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.
- button.action(callable) → this
- button.text(text) → this
Control Methods
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
Button
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#onAction | callable(1) | The onAction handler |
#text | String | The 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.
- gridPane.at(column, row, [columnSpan, rowSpan], node) → this
- gridPane.hgap(pixels) → this
- gridPane.vgap(pixels) → this
Pane Methods
- pane.child(node) → this
- pane.children() → joe.List
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
GridPane
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#alignment | Pos | Alignment of the grid within the widget. |
#gridLinesVisible | Boolean | Whether to draw grid lines for debugging. |
#hgap | Number | Gap between columns in pixels. |
#vgap | Number | Gap 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.
- HBox.getHgrow(node) → Priority
- HBox.getMargin(node) → Insets
- HBox.setHgrow(node, priority)
- HBox.setMargin(node, insets)
- hBox.spacing(pixels) → this
Pane Methods
- pane.child(node) → this
- pane.children() → joe.List
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
HBox
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#alignment | Pos | The default alignment for children |
#spacing | Number | The 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
)
- HPos.values() → List
- hPos.name() → String
- hPos.ordinal() → Number
- hPos.toString() → String
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.
- insets.getBottom() → Number
- insets.getLeft() → Number
- insets.getRight() → Number
- insets.getTop() → Number
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.
- label.text(text) → this
Control Methods
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
Label
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#text | String | The 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.
- listView.getItems() → joe.List
- listView.getSelectedIndex() → joe.Number
- listView.getSelectedItem() → item
- listView.item(item) → this
- listView.items(list) → this
- listView.onSelect(callable) → this
- listView.placeholder(node) → this
- listView.placeholderText(text) → this
- listView.selectIndex(index) → this
- listView.selectItem(item) → this
- listView.stringifier(callable) → this
Control Methods
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
ListViewInstance
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#placeholder | Node | Empty 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.
- menu.disable([flag]) → this
- menu.getProperty(keyword) → value
- menu.id(id) → this
- menu.isDisabled() → joe.Boolean
- menu.item(item) → this
- menu.items() → joe.List
- menu.listenTo(keyword, listener) → this
- menu.properties() → joe.Set
- menu.setProperty(keyword, value) → this
- menu.styleClasses() → joe.List
- menu.styles(style, ...) → this
- menu.text(text) → this
- menu.toString() → joe.String
Properties
All Node
widgets have the following properties.
See Styling with CSS for more on using CSS.
Menu Initializer
Menu() → Menu
Returns a Menu
.
Methods
menu.disable()
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()
menu.getProperty(keyword) → value
Gets the value of the property with the given keyword.
menu.id()
menu.id(id) → this
Sets the menu's #id
property to the given id string.
menu.isDisabled()
menu.isDisabled() → joe.Boolean
Returns true
if the menu has been disabled, and false
otherwise.
menu.item()
menu.item(item) → this
Adds a MenuItem
or Menu
to the menu.
menu.items()
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()
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()
menu.properties() → joe.Set
Returns a readonly Set
of the object's property keywords.
menu.setProperty()
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()
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()
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()
menu.text(text) → this
Sets the menu's text.
menu.toString()
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.
- menuBar.menu(menu) → this
- menuBar.menus() → joe.List
Control Methods
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
MenuBar
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#useSystemMenuBar | Boolean | Use system menu bar. |
MenuBar Initializer
MenuBar([text]) → MenuBar
Returns a MenuBar
.
Methods
menuBar.menu()
menuBar.menu(menu) → this
Adds a Menu
to the menu bar.
menuBar.menus()
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
.
- menuItem.action(callable) → this
- menuItem.disable([flag]) → this
- menuItem.getProperty(keyword) → value
- menuItem.id(id) → this
- menuItem.isDisabled() → joe.Boolean
- menuItem.listenTo(keyword, listener) → this
- menuItem.properties() → joe.Set
- menuItem.setProperty(keyword, value) → this
- menuItem.styleClasses() → joe.List
- menuItem.styles(style, ...) → this
- menuItem.text(text) → this
- menuItem.toString() → joe.String
Properties
All Node
widgets have the following properties.
Property | Type | Description |
---|---|---|
#id | String | JavaFX ID |
#onAction | callable(1) | The onAction handler |
#style | String | FXCSS style string |
#text | String | MenuItem text |
See Styling with CSS for more on using CSS.
MenuItem Initializer
MenuItem() → MenuItem
Returns a MenuItem
.
Methods
menuItem.action()
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()
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()
menuItem.getProperty(keyword) → value
Gets the value of the property with the given keyword.
menuItem.id()
menuItem.id(id) → this
Sets the item's #id
property to the given id string.
menuItem.isDisabled()
menuItem.isDisabled() → joe.Boolean
Returns true
if the item has been disabled, and false
otherwise.
menuItem.listenTo()
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()
menuItem.properties() → joe.Set
Returns a readonly Set
of the object's property keywords.
menuItem.setProperty()
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()
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()
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()
menuItem.text(text) → this
Sets the item's text.
menuItem.toString()
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.
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
All Node
widgets have the following properties.
Property | Type | Description |
---|---|---|
#id | String | JavaFX ID |
#style | String | FXCSS style string |
#visible | Boolean | Visibility 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
)
- Orientation.values() → List
- orientation.name() → String
- orientation.ordinal() → Number
- orientation.toString() → String
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.
- pane.child(node) → this
- pane.children() → joe.List
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
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.
- 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
- Pos.values() → List
- pos.name() → String
- pos.ordinal() → Number
- pos.toString() → String
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
.
- Priority.values() → List
- priority.name() → String
- priority.ordinal() → Number
- priority.toString() → String
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.
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
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.
Property | Type | Description |
---|---|---|
#maxHeight | Number | Maximum height in pixels |
#maxWidth | Number | Maximum width in pixels |
#minHeight | Number | Minimum height in pixels |
#minWidth | Number | Minimum width in pixels |
#padding | Insets | Preferred height in pixels |
#prefHeight | Number | Preferred height in pixels |
#prefWidth | Number | Preferred 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.
- separator.horizontal() → this
- separator.vertical() → this
Control Methods
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
Separator
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#orientation | Orientation | Horizontal 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.
- Side.values() → List
- side.name() → String
- side.ordinal() → Number
- side.toString() → String
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.
- splitPane.getDividers() → joe.List
- splitPane.horizontal() → this
- splitPane.item(item) → this
- splitPane.items() → joe.List
- splitPane.setDivider(index position) → this
- splitPane.setDividers(position,...) → this
- splitPane.vertical() → this
Control Methods
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
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.
Property | Type | Description |
---|---|---|
#orientation | Orientation | Layout 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.
- StackPane.getAlignment(node) → Pos
- StackPane.getMargin(node) → Insets
- StackPane.setAlignment(node, pos)
- StackPane.setMargin(node, insets)
Pane Methods
- pane.child(node) → this
- pane.children() → joe.List
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
StackPane
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#alignment | Pos | The 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.
- tab.content(node) → this
- tab.disable([flag]) → this
- tab.getProperty(keyword) → value
- tab.id(id) → this
- tab.isDisabled() → joe.Boolean
- tab.isSelected() → joe.Boolean
- tab.listenTo(keyword, listener) → this
- tab.properties() → joe.Set
- tab.setProperty(keyword, value) → this
- tab.styleClasses() → joe.List
- tab.styles(style, ...) → this
- tab.tabPane() → TabPane
- tab.text(text) → this
- tab.toString() → joe.String
Properties
All Node
widgets have the following properties.
Property | Type | Description |
---|---|---|
#content | Node | Content node |
#id | String | JavaFX ID |
#style | String | FXCSS style string |
#text | String | Tab 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.
- tabPane.tab(tab) → this
- tabPane.tabs() → joe.List
Control Methods
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
TabPane
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#side | Side | The 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.
- VBox.getMargin(node) → Insets
- VBox.getVgrow(node) → Priority
- VBox.setMargin(node, insets)
- VBox.setVgrow(node, priority)
- vBox.spacing(pixels) → this
Pane Methods
- pane.child(node) → this
- pane.children() → joe.List
Region Methods
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
Node Methods
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
Properties
VBox
widgets have the following properties, in addition to
those inherited from superclasses.
Property | Type | Description |
---|---|---|
#alignment | Pos | The default alignment for children |
#spacing | Number | The 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
)
- VPos.values() → List
- vPos.name() → String
- vPos.ordinal() → Number
- vPos.toString() → String
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.
- Win.css(css) → this
- Win.cssFile(filename) → this
- Win.root() → VBox
- Win.setSize(width, height) → this
- Win.setTitle(title) → this
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.
joe
package (Joe Standard Library)- catch(callable) → CatchResult
- print(text)
- printf(fmt, [values...])
- println([text])
- AssertError extends Error
- Boolean
- CatchResult
- Error
- Error(message, [trace, ...]) → Error
- error.addInfo(message) → this
- error.javaStackTrace() → String
- error.message() → text
- error.stackTrace() → String
- error.traces() → List
- error.type() → name
- Fact
- Function
- Joe
- Keyword
- List
- List.of(items...) → List
- List() → List
- List(other) → List
- List(size, [initValue]) → List
- list.add([index], item) → this
- list.addAll([index], collection) → this
- list.clear() → this
- list.contains(value) → Boolean
- list.containsAll(collection) → Boolean
- list.copy() → List
- list.filter(predicate) → List
- list.get(index) → value
- list.getFirst() → value
- list.getLast() → value
- list.indexOf(value) → Number
- list.isEmpty() → Boolean
- list.lastIndexOf(value) → Number
- list.map(func) → List
- list.peekFirst() → value
- list.peekLast() → value
- list.remove(value) → Boolean
- list.removeAll(collection) → Boolean
- list.removeAt(index) → value
- list.removeFirst() → value
- list.removeLast() → value
- list.reverse() → List
- list.set(index, newValue) → oldValue
- list.size() → Number
- list.sorted([comparator]) → List
- list.sublist(start, [end]) → List
- list.toString() → String
- Map
- Map.of(values...)
- Map([other]) → Map
- map.clear() → this
- map.containsKey(key) → Boolean
- map.containsValue(value) → Boolean
- map.copy() → Map
- map.get(key) → value
- map.getOrDefault(key, defaultValue) → value
- map.isEmpty() → Boolean
- map.keySet() → Set
- map.put(key, value) → value
- map.putAll(map) → this
- map.remove(key) → value
- map.size() → Number
- map.toString() → String
- map.values() → List
- Number
- Number.E
- Number.MAX_INT
- Number.MAX_VALUE
- Number.MIN_INT
- Number.MIN_VALUE
- Number.NAN
- Number.NEGATIVE_INFINITY
- Number.PI
- Number.POSITIVE_INFINITY
- Number.TAU
- Number.abs(num) → Number
- Number.acos(num) → Number
- Number.asin(num) → Number
- Number.atan(num) → Number
- Number.atan2(x, y) → Number
- Number.ceil(num) → Number
- Number.clamp(num, min, max) → Number
- Number.cos(a) → Number
- Number.exp(num) → Number
- Number.floor(num) → Number
- Number.hypot(x, y) → Number
- Number.log(num) → Number
- Number.log10(num) → Number
- Number.max(num...) → Number
- Number.min(num...) → Number
- Number.mod(a, b) → Number
- Number.pow(a, b) → Number
- Number.random() → Number
- Number.round(num) → Number
- Number.sin(a) → Number
- Number.sqrt(num) → Number
- Number.tan(a) → Number
- Number.toDegrees(radians) → Number
- Number.toRadians(degrees) → Number
- Number(string) → Number
- Opaque
- RuleSet
- Set
- Set(values...) → Set
- set.add(value) → Boolean
- set.addAll(collection) → Boolean
- set.clear() → this
- set.contains(value) → Boolean
- set.copy() → Set
- set.filter(predicate) → Set
- set.isEmpty() → Boolean
- set.map(func) → Set
- set.remove(value) → Boolean
- set.removeAll(collection) → Boolean
- set.size() → Number
- set.sorted([comparator]) → List
- set.toString() → String
- String
- String.format(fmt, [values...]) → String
- String.join(delimiter, list) → String
- String(value) → String
- string.charAt(index) → String
- string.contains(target) → Boolean
- string.endsWith(suffix) → Boolean
- string.equalsIgnoreCase(other) → Boolean
- string.indent(n) → String
- string.indexOf(target, [beginIndex], [endIndex]) → Number
- string.isBlank() → Boolean
- string.isEmpty() → Boolean
- string.lastIndexOf(target, [fromIndex]) → Number
- string.length() → Double
- string.lines() → List
- string.matches(pattern) → Boolean
- string.repeat(count) → String
- string.replace(target, replacement) → String
- string.replaceAll(regex, replacement) → String
- string.replaceFirst(regex, replacement) → String
- string.split(delimiter) → List
- string.splitWithDelimiters(delimiter) → List
- string.startsWith(prefix) → Boolean
- string.strip() → String
- string.stripIndent() → String
- string.stripLeading() → String
- string.stripTrailing() → String
- string.substring(beginIndex, [endIndex]) → String
- string.toLowerCase() → String
- string.toString() → String
- string.toUpperCase() → String
- TextBuilder
- Type
joe.console
package- Console
- Path
- Path.compare(a, b) → Number
- Path(first,[more...]) → Path
- path.endsWith(path) → Boolean
- path.getFileName() → Path
- path.getName(index) → Path
- path.getNameCount() → Number
- path.getParent() → Path
- path.isAbsolute() → Boolean
- path.normalize() → Path
- path.relativize(other) → Path
- path.resolve(other) → Path
- path.startsWith(path) → Boolean
- path.subpath(start, [end]) → Path
- path.toAbsolutePath() → Path
- path.toString() → String
joe.doc
package (JoeDoc Configuration API)joe.test
package (Joe Test Tool API)- assertEquals(got, expected)
- assertError(callable, [message], [frames...])
- assertFalse(condition)
- assertTrue(condition)
- check(value) → ValueChecker
- checkCatch(callable) → CatchChecker
- engine() → String
- fail(message)
- skip(message)
- CatchChecker
- CatchChecker(catchResult) → CatchChecker
- catchChecker.isError() → Boolean
- catchChecker.isOK() → this
- catchChecker.message(expected) → this
- catchChecker.stackFrames(frame,...) → this
- catchChecker.stackTrace(expected) → this
- catchChecker.type(expected) → this
- JoeTest
- ValueChecker
- ValueChecker(value) → ValueChecker
- valueChecker.containsAll(values...) → this
- valueChecker.eq(expected) → this
- valueChecker.hasType(type) → this
- valueChecker.hasTypeName(name) → this
- valueChecker.isEmpty() → this
- valueChecker.isFalse() → this
- valueChecker.isNotNull() → this
- valueChecker.isNull() → this
- valueChecker.isTrue() → this
joe.win
package- Button extends Control
- Button([text], [action]) → Button
- button.action(callable) → this
- button.text(text) → this
- Control extends Region
- control.tooltip(tooltip) → this
- control.tooltipText(text) → this
- GridPane extends Pane
- HBox extends Pane
- HPos
- HPos.CENTER
- HPos.LEFT
- HPos.RIGHT
- HPos.values() → List
- HPos(value) → HPos
- hPos.name() → String
- hPos.ordinal() → Number
- hPos.toString() → String
- Insets
- Insets(pixels) → Insets
- Insets(top, right, bottom, left) → Insets
- insets.getBottom() → Number
- insets.getLeft() → Number
- insets.getRight() → Number
- insets.getTop() → Number
- Label extends Control
- Label([text]) → Label
- label.text(text) → this
- ListView extends Control
- ListView() → ListView
- listView.getItems() → joe.List
- listView.getSelectedIndex() → joe.Number
- listView.getSelectedItem() → item
- listView.item(item) → this
- listView.items(list) → this
- listView.onSelect(callable) → this
- listView.placeholder(node) → this
- listView.placeholderText(text) → this
- listView.selectIndex(index) → this
- listView.selectItem(item) → this
- listView.stringifier(callable) → this
- Menu
- Menu() → Menu
- menu.disable([flag]) → this
- menu.getProperty(keyword) → value
- menu.id(id) → this
- menu.isDisabled() → joe.Boolean
- menu.item(item) → this
- menu.items() → joe.List
- menu.listenTo(keyword, listener) → this
- menu.properties() → joe.Set
- menu.setProperty(keyword, value) → this
- menu.styleClasses() → joe.List
- menu.styles(style, ...) → this
- menu.text(text) → this
- menu.toString() → joe.String
- MenuBar extends Control
- MenuItem
- MenuItem() → MenuItem
- menuItem.action(callable) → this
- menuItem.disable([flag]) → this
- menuItem.getProperty(keyword) → value
- menuItem.id(id) → this
- menuItem.isDisabled() → joe.Boolean
- menuItem.listenTo(keyword, listener) → this
- menuItem.properties() → joe.Set
- menuItem.setProperty(keyword, value) → this
- menuItem.styleClasses() → joe.List
- menuItem.styles(style, ...) → this
- menuItem.text(text) → this
- menuItem.toString() → joe.String
- Node
- node.disable([flag]) → this
- node.getProperty(keyword) → value
- node.gridColumn(index) → this
- node.gridColumnSpan(span) → this
- node.gridHalignment(hpos) → this
- node.gridHgrow([priority]) → this
- node.gridMargin(insets) → this
- node.gridRow(index) → this
- node.gridRowSpan(span) → this
- node.gridValignment(vpos) → this
- node.gridVgrow([priority]) → this
- node.hgrow([priority]) → this
- node.id(id) → this
- node.isDisabled() → joe.Boolean
- node.listenTo(keyword, listener) → this
- node.properties() → joe.Set
- node.setProperty(keyword, value) → this
- node.splitResizeWithParent(flag) → this
- node.styleClasses() → joe.List
- node.styles(style, ...) → this
- node.toString() → joe.String
- node.vgrow([priority]) → this
- Orientation
- Orientation.HORIZONTAL
- Orientation.VERTICAL
- Orientation.values() → List
- Orientation(value) → Orientation
- orientation.name() → String
- orientation.ordinal() → Number
- orientation.toString() → String
- Pane extends Region
- Pane() → Pane
- pane.child(node) → this
- pane.children() → joe.List
- Pos
- Priority
- Priority.ALWAYS
- Priority.NEVER
- Priority.SOMETIMES
- Priority.values() → List
- Priority(value) → Priority
- priority.name() → String
- priority.ordinal() → Number
- priority.toString() → String
- Region extends Node
- Region.USE_COMPUTED_SIZE
- Region.USE_PREF_SIZE
- region.height(height) → this
- region.padding(pixels) → this
- region.padding(top, right, bottom, left) → this
- region.prefHeight(height) → this
- region.prefWidth(width) → this
- region.width(width) → this
- Separator extends Control
- Separator() → Separator
- separator.horizontal() → this
- separator.vertical() → this
- Side
- Side.BOTTOM
- Side.LEFT
- Side.RIGHT
- Side.TOP
- Side.values() → List
- Side(value) → Side
- side.name() → String
- side.ordinal() → Number
- side.toString() → String
- SplitPane extends Control
- StackPane extends Pane
- Tab
- Tab() → Tab
- tab.content(node) → this
- tab.disable([flag]) → this
- tab.getProperty(keyword) → value
- tab.id(id) → this
- tab.isDisabled() → joe.Boolean
- tab.isSelected() → joe.Boolean
- tab.listenTo(keyword, listener) → this
- tab.properties() → joe.Set
- tab.setProperty(keyword, value) → this
- tab.styleClasses() → joe.List
- tab.styles(style, ...) → this
- tab.tabPane() → TabPane
- tab.text(text) → this
- tab.toString() → joe.String
- TabPane extends Control
- TabPane([text]) → TabPane
- tabPane.tab(tab) → this
- tabPane.tabs() → joe.List
- VBox extends Pane
- VPos
- VPos.BASELINE
- VPos.BOTTOM
- VPos.CENTER
- VPos.TOP
- VPos.values() → List
- VPos(value) → VPos
- vPos.name() → String
- vPos.ordinal() → Number
- vPos.toString() → String
- Win
- Win.css(css) → this
- Win.cssFile(filename) → this
- Win.root() → VBox
- Win.setSize(width, height) → this
- Win.setTitle(title) → this
- Button extends Control
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. TheCompiler
must not compile an arbitrary expression betweenTSET
andTGET
, as it might step on the stashed value.
Instruction Set
Mnemonic/argument | Stack effect | Description |
---|---|---|
ADD | a b → c | c = a + b |
ASSERT | msg → ∅ | Throws AssertError |
CALL argc | f args → c | c = f(args) |
CLASS name | ∅ → type | Create class |
CLOSURE func... | ∅ → f | Load closure |
COMMENT name | ∅ → ∅ | No-op comment |
CONST constant | ∅ → a | Load constant |
DECR | a → b | b = a - 1 |
DIV | a b → c | c = a/b |
DUP | a → a a | Duplicate top |
DUP2 | a b → a b a b | Duplicate top 2 |
EQ | a b → c | c = a == b |
FALSE | ∅ → false | Load false |
GE | a b → c | c = a >= b |
GETNEXT | iter → a | a = iter.next() |
GLOBIND | p t → flag | Bind target to pattern |
GLOBINO | p t → ∅ | Bind target to pattern |
GLODEF name | a → ∅ | Define global |
GLOGET name | ∅ → a | Get global |
GLOSET name | a → a | Set global |
GT | a b → a > b | Compare: greater |
HASNEXT | iter → flag | flag = iter.hasNext() |
IN | a coll → flag | a in collection |
INCR | a → b | b = a + 1 |
INDGET | coll i → a | a = coll[i] |
INDSET | coll i a → a | coll[i] = a |
INHERIT | sup sub → sup | Inheritance |
ITER | coll → iter | iter = coll.iterator() |
JIF offset | flag → ∅ | Jump if false |
JIFKEEP offset | flag → flag | Jump if false, keep value |
JIT offset | flag → ∅ | Jump if true |
JITKEEP offset | flag → flag | Jump if true, keep value |
JUMP offset | ∅ → ∅ | Jump forwards |
LE | a b → a <= b | Compare: less or equal |
LISTADD | list a → list | Add item to list |
LISTNEW | ∅ → list | Push empty list |
LOCBIND | p t → vs | Bind target to pattern |
LOCGET slot | ∅ → a | Get local |
LOCMOVE slot n | ... vars → vars ... | Get local |
LOCSET slot | a → a | Set local |
LOOP offset | ∅ → ∅ | Jump backwards |
LT | a b → a <= b | Compare: less than |
MAPNEW | ∅ → map | Push empty ma p |
MAPPUT | map k a → map | Add entry to map |
MATCH | p t → vs? flag | Match pattern to target |
METHOD name | type f → type | Add method to type |
MUL | a b → c | c = a*b |
NE | a b → c | c = a != b |
NEGATE | a → b | b = -a |
NI | a coll → flag | a not in collection |
NOT | a → b | b = !a |
NULL | ∅ → null | Load null |
PATTERN p bs | constants → pv | Evaluate the pattern |
POP | a → ∅ | Pops one value |
POPN n | a... → ∅ | Pops n values |
PROPGET name | obj → a | Get property value |
PROPSET name | obj a → a | Set property value |
RECORD name fields | ∅ → type | Create record type |
RETURN | a → a | Return |
RULESET ruleset | exports → rsv | Evaluate rule set |
SUB | a b → c | c = a - b |
SUPGET name | obj sup → f | Get superclass method |
SWAP | a b → b a | Swap top stack items |
TGET | ∅ → a | a = T |
THROW | a → ∅ | Throw error |
TPUT | a → ∅ | T = a, pop a |
TRCPOP | ∅ → ∅ | Pops a post-trace |
TRCPUSH trace | ∅ → ∅ | Pushes a post-trace |
TRUE | ∅ → true | Load true |
TSET | a → a | T = a |
UPCLOSE n | v... → ∅ | Closes n upvalue(s) |
UPGET slot | ∅ → a | Get upvalue |
UPSET slot | a → a | Set 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 b → c) 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 aProxyType<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
orfalse
. - func: A scripted function definition: the
Function
itself plus its upvalue data, suitable for building into aClosure
in the current scope. Used only by theCLOSURE
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 b → c, 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 args → c
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.
- The
- For each
Upvalue
in theFunction
:isLocal
: 1 if theUpvalue
is local to the immediately enclosing scope, and 0 if it is from an outer scope.index
: TheUpvalue
'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 | a → b, 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 b → c, where c = (a/b)
Computes the quotient of a and b.
DUP
DUP | a → a a
Duplicates the value on the top of the stack.
DUP2
DUP2 | a b → a b a b
Duplicates the top two values on the stack.
EQ
EQ | a b → c, 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 b → c, where c = (a >= b)
Yields true
if a is greater than or equal to b, and false
otherwise.
GETNEXT
GETNEXT | iter → a
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 t → flag
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 | a → a
Assigns value a to global variable name, leaving a on the stack.
GT
GT | a b → c, where c = (a > b)
Yields true
if a is greater than or equal to b, and false
otherwise.
HASNEXT
HASNEXT | iter → flag
Gets whether iterator iter has another value. The iter must be an
iterator created by the ITER
instruction.
IN
IN | a coll → flag
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 | a → b, 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 i → a
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 aMap
.
INDSET
INDSET | coll i a → a
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 aMap
.
INHERIT
INHERIT | sup sub → sup
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 | coll → iter
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 | flag → flag
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 | flag → flag
Jumps forward by offset if flag is true, retaining the flag on the stack.
JUMP
JUMP offset | ∅ → ∅
Jumps forward by offset.
LE
LE | a b → c, where c = (a <= b)
LISTADD
LISTADD | list a → list
Pops a and adds it to the list value.
LISTNEW
LISTNEW | ∅ → list
Pushes an empty list value onto the stack.
LOCBIND
LOCBIND | p t → vs
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 | ... vars → vars ...
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 | a → a
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 b → c, where c = (a < b)
MAPNEW
MAPNEW | ∅ → map
Pushes an empty map value onto the stack.
MAPPUT
MAPPUT | map k a → map
Pops k and a and puts {k: a} into the map value.
MATCH
MATCH | p t → vs? 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 t → flag
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 t → vs? 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 f → type
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 b → c, where c = (a != b)
Pushes true
if a and b are not equal, and false otherwise.
NEGATE
NEGATE | a → b, where b = (-a)
Negates a.
NI
NI | a coll → flag
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 | a → b, where b = (!a)
NULL
NULL | ∅ → null
Pushes null
on the stack.
PATTERN
PATTERN p bs | constants → pv
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 | obj → a
Gets the value of property name of object obj.
PROPSET
PROPSET name | obj a → a
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 | a → a
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 | exports → rsv
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 sup → f
Retrieves method name for superclass sup of object obj.
This is used to compile the super.<method>
syntax.
SWAP
SWAP | a b → b 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 | a → a
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 | a → a
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.