An unobtrusive scripting language
paw is a high-level, imperative, dynamically-typed programming language intended for embedding into larger projects.
paw is heavily inspired by the Lua project. This can be seen in the language syntax, as well as the design of the VM. Like the Lua VM, paw's VM is reentrant, meaning a C function called from paw can call other paw functions. paw also restricts the language grammer to eliminate the need for semicolons (most-notably: assignments are not expressions). Additionally, paw uses a quick single-pass compiler and is easy to embed.
- Correctness: This is the most-important goal, by far.
A language that returns incorrect results can't be very useful, so of course, paw should be implemented correctly.
The language should be designed for human readability, and eliminate syntax foot-guns where possible (for example, an
if
statement without '{}' followed by 2 indented lines will not guard the second line). paw code should never invoke undefined behavior (UB), and the C interface should be carefully documented (since, of course, it is possible to have UB there). - Performance: paw should be (relatively) fast, possibly to the detriment of portability, but never at the expense of correctness. Being dynamically-typed and hosted, paw will never achieve the same performance as C, the statically-typed host language. This makes interoperating with C particularly important, since it is likely users will want to call C functions to perform computationally-intensive work.
- Ergonomics: paw should be easy to use, and easy to learn.
paw supports both line- and block-style comments. Nesting is not allowed in block-style comments.
-- line comment
-* block
comment *-
Any variable referenced in the runtime must first be declared: either by a let
statement, or with the C API.
Otherwise, a "name error" is raised (see the section on error handling below).
Variables declared at the module level are considered global variables, otherwise, they are locals.
Global variables can be referenced from anywhere in the program, while locals can only be referenced where they are visible (see scope).
Locals can be captured in a function or class definition (see functions).
let x -- short for 'let x = null'
let x = 123 -- rebind 'x' to 123
paw is dynamically-typed, meaning that variable types are determined at runtime. Every paw value contains 2 fields: a type tag and a value (essentially a tagged union). NaN boxing is used to pack both of these fields into 8 bytes of memory. The following example demonstrates creation of the basic value types.
let null_ = null
let boolean = true
let integer = 0x123
let float_ = 10.0e-1
let array = [1, 2, 3]
let map = {'a': 1, 2: 'b'}
let function = fn() {return 42}
class Class {
method(self) {}
}
let instance = Class()
let method = instance.method
paw implements lexical scoping, meaning variables declared in a given block can only be referenced from within that block, or one of its subblocks. A block begins when a '{' token is encountered that is not the start of a map literal, and ends when a matching '}' is found. Many language constructs use blocks to create their own scope, like functions, classes, for loops, etc. paw also provides raw blocks for exerting finer control over variable lifetimes.
{
let x = 42
} -- 'x' goes out of scope here
Functions are first-class in paw, which means they are treated like any other paw value. Functions can be stored in variables, or passed as parameters to compose higher-order functions.
fn fib(n) {
if n < 2 {
return n
}
return fib(n - 2) + fib(n - 1)
}
fib(10)
-- Anonymous functions:
let add = fn(a, b) {
return a + b
}
class Superclass {
__init(value) {
-- 'self' is an implicit parameter
self.value = value
}
}
class Class: Superclass {
-- metamethod for initialization: called when 'Class(x)' is encountered
__init(value) {
-- call Superclass.__init(self)
super.__init(value)
}
-- normal class method
method() {
return self.value
}
}
-- create an instance
let c = Class()
Metamethods are how paw implements operator overloading.
A metamethod is a function bound to an instance object that is called when the object is used in a specific operation.
Metamethod names are always prefixed with 2 underscores, i.e. __add
.
Many binary operators have a 'reverse' metamethod that is called when the receiver is not the first operand.
For example, __radd
, the reverse metamethod for __add
is called to evaluate the expression 1 + x
, where x
is an instance.
It is an error is x
does not have the __radd
metamethod.
Relational comparisons are handled similarly to binary operators.
In the expression 1 < x
, we cannot call __lt
, since x
is not on the left-hand side.
Instead we attempt x.__ge(1)
and negate the result.
Operation | Metamethod | Reverse metamethod |
---|---|---|
- | __null |
- |
str |
__str |
- |
int |
__int |
- |
float |
__float |
- |
bool |
__bool |
- |
array |
__array |
- |
map |
__map |
- |
f() |
__call |
- |
o.a |
__getattr |
- |
o.a = v |
__setattr |
- |
o[i] |
__getitem |
- |
o[i] = v |
__setitem |
- |
o[i:j] |
__getslice |
- |
o[i:j] = v |
__setslice |
- |
== |
__eq |
- |
< |
__lt |
__gt |
<= |
__le |
__ge |
> |
__gt |
__lt |
>= |
__ge |
__le |
in |
__contains |
- |
# |
__len |
- |
- |
__neg |
- |
! |
__not |
- |
~ |
__bnot |
- |
+ |
__add |
__radd |
- |
__sub |
__rsub |
* |
__mul |
__rmul |
/ |
__div |
__rdiv |
// |
__idiv |
__ridiv |
% |
__mod |
__rmod |
++ |
__concat |
__rconcat |
^ |
__bxor |
__rbxor |
& |
__band |
__rband |
| |
__bor |
__rbor |
<< |
__shl |
__rshl |
>> |
__shr |
__rshr |
class Class {
__init(value) {
self.value = value
}
-- metamethod for '+': called when 'Class(lhs) + rhs' is encountered
__add(rhs) {
let value = self.value ++ rhs
return Class(value)
}
-- reverse metamethod for '+': called when 'lhs + Class(rhs)' is encountered
__radd(lhs) {
let value = lhs ++ self.value
return Class(value)
}
-- metamethod for '=='
__eq(rhs) {
return self.value == rhs
}
-- metamethod for controlling null chaining and null coalescing operators
__null() {
-- Return null if this object is semantically null, nonnull otherwise.
-- If null is returned, then the expression 'x?' will return 'x' from
-- the enclosing function (not null), and 'x ?: 123' will evaluate to
-- '123'. If nonnull is returned, then both 'x?' and 'x ?: 123' will
-- evaluate to this function's return value (not 'x').
return self.value < 0 ?? null :: self
}
}
-- Instances of 'Class' can be equality-compared with, and added to, numeric
-- values.
assert(3 == Class(1) + 2)
assert(3 == 1 + Class(2))
paw supports many common types of control flow.
-- 'if-else' statement:
if i == 0 {
} else if i == 1 {
} else {
}
-- Conditional (ternary) expressions:
let v = cond ?? 'then' :: 'else'
-- Null chaining operator: return immediately (with null) if the operand is null
-- A paw module is actually considered a function, so '?' can exist at the top
-- level.
let v = maybe_null()?.field?
-- Null coalescing operator: evaluates to the first operand if it is nonnull, the
-- second operand otherwise. The second expression is not evaluated if the first
-- operand is null.
let v = a ?: b
-- 'break'/'continue' (must appear in a loop):
break
continue
-- Numeric 'for' loop:
for i in 0, 10, 2 { -- start, end, step
}
-- Iterator 'for' loop: allows iterating over arrays and maps. If a class implements
-- both '__getitem' and '__len', then instances of that class can be used in an
-- iterator 'for' loop.
for v in iterable {
}
-- 'while' loop:
let i = 0
while i < 10 {
i = i + 1
}
-- 'do...while' loop:
let i = 10
do {
i = i - 1
} while i > 0
let s = 'Hello, world!'
assert(s.starts_with('Hello'))
assert(s.ends_with('world!'))
assert(s[:5].ends_with('Hello'))
assert(s[-6:].starts_with('world!'))
assert(1 == s.find('ello'))
assert(-1 == s.find('Cello'))
let a = s.split(',')
assert(s == ','.join(a))
let a = [1, 2, 3]
assert(a[:1] == [1])
assert(a[1:-1] == [2])
assert(a[-1:] == [3])
let m = {1: 'a', 'b': true}
m[3] = 42
m.erase(1)
-- prints 'default'
print(m.get(1, 'default'))
fn divide_by_0(n) {
return n / 0
}
let status = try(divide_by_0, 42)
assert(status != 0)
Precedence | Operator | Description | Associativity |
---|---|---|---|
16 | () [] . ? |
Call, Subscript, Member access, Null chain | Left |
15 | ! - ~ |
Not, Negate, Bitwise not | Right |
14 | * / // % |
Multiply, Divide, Integer divide, Modulus | Left |
13 | + - |
Add, Subtract | Left |
12 | ++ |
Concatenate | Left |
11 | << >> |
Shift left, Shift right | Left |
10 | & |
Bitwise and | Left |
9 | ^ |
Bitwise xor | Left |
8 | | |
Bitwise or | Left |
7 | in < <= > >= |
Inclusion, Relational comparisons | Left |
6 | == != |
Equality comparisons | Left |
5 | && |
And | Left |
4 | || |
Or | Left |
3 | ?: |
Null coalesce | Left |
2 | ??:: |
Conditional | Right |
1 | = |
Assignment | Right |
- Add a few things to the C API:
- Better way to call builtin functions and methods on builtin types
- Better API for arrays:
paw_*_item
will throw an error if the index is out of bounds
- For loops won't work with bigint right now.
- Finish designing things first...
- Language features:
**
(pow) operator- Slicing syntax
- Spread operator, used in call expressions, assignments/let statements, and array literals
- Multi-return/let/assign with Lua semantics?
- Concurrency: fibers, coroutines?
- Language features:
- Documentation
- Make it fast!