A very simple scripting language. Each line can be parsed in isolation and stored as compressed tokens. This makes it more memory efficient than Python or Lua, while it has more modern features than BASIC (like maps).
IMPORTANT NOTE: This project is a work-in-progress and is not complete. Expect nothing to work right.
# We have functions
fn foo( x )
# Simple logical expressions
if bar( x ) > 0
return 0
end
# We have dynamic typing
# Variables are created with the `var` statement
# This creates a new variable in this scope, shadowing any variables
# with the same name created in a different (earlier) scope
var z = x + 1
# We update an existing variable with the `let` statement
let z = z + 1
# We have hashmaps
var m = map()
m.set("key2", "hi")
# If we read a key that doesn't exist, we get `nil`
print("getting unknown key returns {}", m.get("unknown"))
# We have a special map called `globals`, which is always in scope
globals.set("x", x)
# We have vectors, which are growable ordered lists of data
var v = vec()
# We have 'method' calls, so this...
v.push( 1 )
# ...is the same as
vector.push(v, 1)
# Note that function arguments are always copied, but vectors and hashmaps are
# reference counted, and so the function gets a copy of the reference. This
# is similar to Python.
v.push( format( "Hi {}", z ) )
v.push( m )
# We have BASIC style for loops
for idx = 0 to len( v ) - 1
# Variables have block scope
var y = idx + 1
# We can print things, using `{}` to insert values.
# We can also index a vec with `[]`, and we can get the type
# of a variable with the `type` function.
print( "v[{}] = {} ({})", idx, v[ idx ], type( v[ idx ] ) )
if v[ idx ] == 1
# We can break out of loops too
break
end
end
# The variable y no longer exists here
return z
end
# We also have modules
module baz
fn foo()
# Accessed as baz.foo()
end
end
# We even have classes
# Internally each class has a Hashmap (for class variables), and each Object has a Hashmap (for object members)
class Point
var classVariable = "Some String"
fn __init__(self, x, y)
# This sets the `x` member of the new object's internal hashmap
self.x = x
self.y = y
end
# This is called automatically when we convert objects of this class to a string
fn __str__(self)
var s = format("Point(x={},y={}", self.x, self.y)
return s
end
end
fn testPoint()
var m = Point.new(1, 2)
# Look ups happen in the object dictionary first, then the class dictionary
print("m = {} ({})", string(m), m.classVariable)
end
A variable can be of the following types:
- Scalars:
- Integer (signed, 32-bit)
- Float (32-bit)
- String (immutable, UTF-8 encoded)
- Bytes (immutable ordered collection of 8-bit bytes)
- Boolean (true or false)
- Nil
- Collections:
- Vector (an ordered collection of values of any type)
- Map (an associative array, or dictionary, where the keys are String, the values any type)
- Classes (a collection of class variables and methods)
- Objects (an instance of a class, with its own variables)
To save memory, internally a String
distinguishes between a pointer to a
string-literal in the program source, and a heap-allocated string which has
been created during program execution (e.g. with the format
function).
The language is dynamically typed, so a variable remembers at run time both its type and its value. Scalars are passed to functions by value, and have Copy semantics. Collections are passed by reference, and have reference-counting semantics (i.e. a copy of the reference is passed).
Variables have block scope (that is inside function, for loop, loop and if/elsif/else statement blocks) and collections are only freed when they hit a reference count of zero.
You cannot create global variables. Instead, there is a single global variable
called globals
, which is a map.
Rather than entering plain-text source code into a file and then running a separate compilation step as part of the script execution (like Python or JavaScript), Neotrotronian instead is a line-based language. Each line is entered, parsed and stored in tokenised form, line by line. You can ask the interface to list the program as it stands, which will convert the tokenised form back into the canonical source code representation, including indentation. You can also ask the interface to delete lines, delete a range of lines, edit an existing line or insert a new line. This minimises the use of memory and avoids a separate pre-compilation step. It also means that a disk drive, or indeed any kind of filing system, is not required to use this language.
When filesystem support is available, programs can be loaded and saved in their plain-text format, or as tokenised data.
The following block is entered if bool({expr})
is true.
An optional extra checked block for an if
statement.
The optional final block for an if
statement.
Starts a finite loop.
Starts an infinite loop. Same as while true
Exits out of the innermost for
or loop
block and moves past the
corresponding end
.
Assigns the value of {expr}
to the pre-existing variable called {var}
.
Creates a new variable called var
. Defaults to nil
if {expr}
isn't supplied.
Start a new function block. You can have zero or more parameters and an
optional final "..." which means any number of parameters are allow - these
are bundled into a Vec called args
.
Closes out the most recent if
, loop
, module
or fn
block.
Starts a new module called {name}
. A module is really just a namespace for functions. It can contain further modules. Maybe one day
we'll let you load modules off disk.
Any line which doesn't start with one of the above, is treated as an
expression-statement. Typically used to call a function where you don't care
about the return value. Broadly equivalent to let _ = {expr}
, except _
isn't a thing.
The following functions are always in scope.
Print takes a string, then an arbitrary number of arguments. The string is
taken as a format string, and any {}
sub-strings are replaced with the
following arguments, in order. Any non-string argument is converted to a
string for display using the string()
function.
Like print
, but adds a new-line character at the end.
Moves the text cursor to column x and row y.
If bool(x)
is true, the cursor is enabled. Otherwise, the cursor is disabled.
Returns the number of rows of text on the screen as an integer. Typically 25.
Returns the number of columns of text on the screen as an integer. Typically 40, 48 or 80.
Sets the foreground colour (i.e. the text colour) to the given value.
- Black: 0 or "black"
- Blue: 1 or "blue"
- Green: 2 or "green"
- Cyan: 3 or "cyan"
- Red: 4 or "red"
- Magenta: 5 or "magenta"
- Yellow: 6 or "yellow"
- White: 7 or "white"
Sets the background colour. See foreground
for the list of colours.
Converts a variable of any type to a string. If any
is a collection, the
collections contents are rendered into the string using [x, y]
or
{key: value}
syntax familar from Python or JavaScript.
Converts integers, strings, booleans and floats to their integer equivalent.
Nil, Vec and Map convert to 0. The prefixes 0x
and 0b
on a String change
the base to 16 or 2 respectively.
Converts integers, strings, booleans and floats to their floating-point equivalent. Nil, Vecs and Maps convert to 0.0.
- Integers and Floats are true if they are non-zero.
- Strings, Vecs and Maps convert to false if they are empty (i.e. have length zero), and true otherwise.
- Nil is false.
- Returns the length, in bytes, of a String
- Returns the length, of a Vec
- Returns the number of values in a Map
- The length of a non-String scalar is zero.
Returns all the keys in a Map as a Vec.
NB: We might make this more efficient in future if we get iterator support.
Removes a value from a collection (and returns it). If the collection is a
Map, key_or_idx
should be a String. If the collection is a Vec, then
key_or_idx
should be an Integer.
Adds a new value to the end of a Vec.
Adds a new value to the end of a Vec.
Like print, but returns a heap-allocated String.
The usual selection of mathematical functions are available, which take floating point values (or integers, which are converted to floats automatically). The trigonometric functions take angles in Radians.
Change the screen mode. Support for various text/graphics modes depends on your OS.
Get the width and height of a bitmap display, in pixels.
Draw a point on a bitmap screen at position x, y in the current foreground colour. Position 0, 0 is the top left of the screen.
Move to position x, y without drawing anything.
Draw a line from the current x, y position to the given x, y position, in the foreground colour.
Draw (and optionally fill in) a rectangle.
Draw (and optionally) fill a circle, or segment of a circle, with the given centre point and radius.
Draw (and optionally) fill an ellipse, or segment of a ellipse, with the given centre point, major radius and minor radius.
Perform a flood fill at the given x, y position. The flood fill will move outwards until it reaches a pixel of a different colour to that at the given x, y position. Only the four compass points are checked (up, down, left and right), not the four diagonals. The screen is filled with the current foreground colour.
Makes a sound, on the given channel (typically 1..3), using the given waveform (typically "square", "sine" or "triangle"), at the given volume (0..255) for a given duration (in 60 Hz frame ticks).
Sleep for the given number of 60 Hz frame ticks.
Wait for the next vertical-blanking interval, when it should be safe to draw on the screen without tearing.
Get the current POSIX time as a float. The time is in local-time, and time-zones are an OS matter.
Get the current Gregorian calendar date/time as a Map. The time is in local-time, and time-zones are an OS matter.
- year (e.g. 2020)
- month (1..12)
- day (1..31)
- hour (0..23)
- minute (0..59)
- second (0..60)
- dow (0..6 where 0 is Monday)
Set the current Gregorian calendar date/time, using the given Map. It must have the following Integer values:
- year (e.g. 2020)
- month (1..12)
- day (1..31)
- hour (0..23)
- minute (0..59)
- second (0..60)
Prints the prompt string, then reads a string from standard-input until Enter is pressed or a control character (Ctrl-A through Ctrl-Z) is entered.
Returns a character (as a single character string) if a key has been pressed and a character is in standard-input buffer, otherwise returns Nil.
If bool(x) is true, puts the console in raw
mode, otherwise leaves raw
mode. In raw
mode,
you must read keyboard events with readevent()
rather than using readkey()
. This is a much
better mode for writing games.
Read a raw keyboard event. If an event is available, it is returned as an integer, otherwise returns Nil.
Open a file. Returns an integer file handle, or Nil if there was an error.
Closes a previously opened file.
Reads bytes from a file, at the current offset, as a Vec of Integers, each 0..255.
Reads UTF-8 bytes from a file and returns a String. If the bytes aren't valid UTF-8, you get Nil.
Returns True if the current offset is at the end of the file.
Reads UTF-8 bytes from a file until a new-line character (or EOF) is found, and returns a String.
Writes bytes to a file. If data is a Vec, every item in the Vec is converted to an integer and then only the bottom 8 bits written. If data is a String, the String is written as UTF-8 encoded bytes. If any other type is provided, it is converted to a String first.
Seeks to a byte offset in the file. Whence should be the string "set", "end" or "current", and controls how the offset is interpreted (as absolute, relative to the end of the file, or relative to the current offset). Offset will be converted to an integer.
Open a directory
Read a directory entry
Closes a directory.
Get stats for a file as a Map.
Get the most recent error code from the OS as a String.
The following constants exist everywhere as part of the standard library.
The file handle for standard output.
The file handle for standard input.
The file handle for standard error.
The floating point value 3.141592...
- Which is better
let x = expr()
,x = expr()
, orx := expr()
? - Which is better
if x == 1
, orif x = 1
? - Which is better
if expr()
, orif expr() then
? - Should we support tuples?
- Should you create a Vec with
vec()
or[]
? - Should you create a Map with
map()
or{}
? - Should we support dot-notation (
my_variable.function()
)? How does that map to a function? - Can functions be stored in variables?
- Do we have lambdas?
- Do lambdas capture local scope?
- If so, is that implicit or explicit?
- Should we distinguish between procedures and functions?
- Is a bare expression a valid statement?
- Should we support modules, or prefix stdlib functions with
module_
, or just lump it all together like C and PHP?
This Rust-language intepreter for the Neotronian language is licensed under the GPL v3.
The language specification, this README, and any example programs in this repository, are available under an MIT licence, Apache-2.0 or under CC0, as your option.
Any PRs to this repository will only be accepted if they are compatible the licensing terms above. You will retain the copyright in any contributions, and must confirm that you have the right to place the contribution under the licences above.