I am moving the documentation to Confluence
Zurfur is a programming language I'm designing for fun and enlightenment. The language is named after our cat, Zurfur, who was named by my son. It's spelled ZurFUR because our cat has fur. The syntax is still being developed and nothing is set in stone. If you want to try it, click here https://gosub.com/zurfur
I love C#. It's my favorite language to program in. But, I'd like to have some features from other languages built in from the ground up. I'm thinking about ownership, immutability, nullability, and functional programming.
Status update: Still porting to https://avaloniaui.net so it runs in the browser.
Zurfur takes its main inspiration from C#, but borrows syntax and design concepts from Lobster, Zig, Midori, Golang, Rust, Python, JavaScript, and other languages.
- Prime directives:
- Fun and easy to use
- Faster than C# and unsafe code just as fast as C
- Target WebAssembly in the browser with easy JavaScript interop
- Ownership, mutability, and nullabilty are part of the type system:
ro
means read only all the way down (not like C#, wherereadonly
only protects the top level)- All types are values (i.e. owned) except for
ro
types (e.g.str
), pointers (e.g.^MyType
) and borrowed references (e.g.&myValue
) - All mutable types have a
ro
counterpart which can be copied quickly via single pointer assignment (e.g.str
isro List<byte>
) - Function parameters must be explicitly marked
mut
if they mutate anything - References and pointers are non-nullable, but may use
?MyType
or?^MyType
for nullable - Deterministic destructors (e.g.
FileStream
closes itself automatically)
- Fast and efficient:
- Return references and span used everywhere.
[]int
isSpan<int>
- Functions pass parameters by reference, but will pass a copy when it is more efficient
- Explicit
copy
required when copying an object that requires dynamic allocation - Most objects are deleted without needing GC. Heap objects are reference counted.
- Return references and span used everywhere.
NOTE: I am still investigating different ways to introduce local variables:
@a = getList() // a is assignable, the list is immutable
@b = mut getList() // a is assignable, the list is mutable
TBD: let
and var
refer to assignablility:
let
for un-assignable, mut
for mutable, and var
for assignable. For example:
let a = getList() // a is un-assignable, the list is immutable
mut b = getList() // b is un-assignable, the list is mutable
var c = getList() // c is assignable, the list is immutable
var mut d = getList() // d is assignable, the list is mutable
The ones we all know and love:
nil, bool, i8, byte, i16, u16, i32, u32, int, u64, f32, float, str
int
and float
are 64 bits wide. str
is an array of bytes.
Type | Description |
---|---|
List<T> | Re-sizable mutable list of elements. ro List\<T\> is the immutable counterpart. |
Span<T> | A view into a List . It has a constant length. Mutability of elements depends on usage (e.g Span from ro List is immutable, Span from List is mutable) |
Map<K,V> | Unordered mutable map. ro Map<K,V> is the immutable counterpart. |
Maybe<T> | Identical to ?T . Always optimized for pointers and references. |
Result<T> | Same as !T . An optional containing either a return value or an Error interface. |
Error | An interface containing a message string and an integer code |
str, str16 | A ro List<byte> or ro List<u16> with support for UTF-8 and UTF-16. str16 is a JavaScript or C# style Unicode string |
All types have a compiler generated ro
counterpart which can be copied
very quickly since cloning them is just copying a reference without dynamic
allocation.
At the module level, functions, methods, and types are private to that
module and it's children unless the [pub]
qualifier is specified.
Fields are public by default but can be made private by prefixing them
with an _
underscore. Private fields can have public getters and setters.
The scope of a private variable is the file that it is declared in.
[pub] // Make this type public
type Example
list1 List<int> = [1,2,3] // Public, initialized with [1,2,3]
_list2 List<int> // Private, initialized with []
_list3 List<int> pub let // Private, but with public read-only access
_list4 List<int> pub let mut // Private, but with public modify, but not assignable
The public getter or setter has the same name as the private field, except without the leading _
.
Strings (i.e. str
) are immutable byte lists (i.e. ro List<byte>
), generally
assumed to hold UTF8 encoded characters. However, there is no rule enforcing
the UTF8 encoding so they may hold any binary data.
String literals start with a quote "
(single line) or with """
(multi-line), and
can be translated at runtime using tr"string"
syntax. They are interpolated
with curly braces (e.g "{expression}"
). Control characters may be put inside
an interpolation (e.g. "{\t}"
is a tab).
There is no StringBuilder
type, use List<byte>
instead:
let sb = mut List<byte>()
sb.push("Count from 1 to 10: ")
for count in 1..+10
sb.push(" {count}")
return sb.toStr()
Span is a view into a List
, ro List
, or str
, etc.. They are type ref
and
may never be stored on the heap. Unlike in C#, a span can be used to pass
data to an async function.
The declaration syntax []Type
translates to Span<Type>
. The following
definitions are identical:
// The following definitions are identical:
fun writeData(data Span<byte>) !int
fun writeData(data []byte) !int
Mutating the len
or capacity
of a List
(not the elements of it) while
there is a Span
or reference pointing into it is a programming error, and
fails the same as indexing outside of array bounds.
let list = mut List<byte>()
list.push("Hello Pat") // list is "Hello Pat"
let slice = mut list[6..+3] // slice is "Pat"
slice[0] = "M"[0] // slice is "Mat", list is "Hello Mat"
list.Push("!") // Runtime failure with stack trace in log file
TBD: Consider how to break
out of the lambda. Use a return type of Breakable
?
Operator precedence is mostly from Golang, but more compatible with C and gives an error where not compatible:
Operators | Notes |
---|---|
x.y f<type>(x) x.(type) a[i] |
Primary |
- ~ & ref not sizeof typeof unsafe |
Unary |
@ | Capture new variable |
? | Use default for Maybe |
! | For Result and Maybe , generate value or throw error when nil |
!!! | For Result and Maybe , generate value or panic when nil |
is is not as |
Type conversion and comparison |
<< >> | Bitwise shift (can't mix arithmetic and bit operators, TBD: always require parentheses) |
* / % & | Multiply, divide, modulus, and bitwise AND (can't mix arithmetic and bit operators) |
~ | Bitwise XOR (can't mix with arithmetic operators) |
+ - | | Add, bitwise OR (can't mix arithmetic and bit operators) |
.. ..+ | Range (Low..High) and range count (Low..+Count). Inclusive of low, exclusive of high. |
== != < <= > >= === !== in not in |
Not associative, === and !== is only for pointers |
and |
Conditional and, short circuit |
or |
Conditional or, short circuit |
a ?? b : c |
Ternary operator. Not associative, no nesting (see below for restrictions) |
=> | Lambda |
key:value | Key value pair (only inside () , [] or where expected) |
, | Comma Separator (not an expression) |
= += -= *= /= %= &= | = ~= <<= >>= |
The ~
operator is both xor and unary complement, same as ^
in Golang.
The @
operator captures the expression into a new variable.
The !
opererator passes an error up to the caller when a Result
has an
Error
. For example while stream.read(buffer)!@length != 0
passes
an error up to the caller, or captures the value returned by read
into the
new variable length
.
The range operator ..
takes two int
s and make a Range
which is a
type Range(High int, Low int)
. The ..+
operator also makes a
range, but the second parameter is a count (High = Low + Count
).
Operator ==
does not default to object comparison, and only works when it
is defined for the given type. Use ===
and !==
for object comparison.
Comparisons are not associative, so a == b == c
is illegal.
TBD: The ternary operator is not associative and cannot be nested.
Examples of illegal expresions are c1 ?? x : c2 ?? y : z
(not associative),
c1 ?? x : (c2 ?? y : z)
(no nesting). The result expressions may not
directly contain an operator with lower precedence than range.
For example, a==b ?? x==3 : y==4
is illegal. Parentheses can be
used to override that behavior, a==b ?? (x==3) : (y==4)
and
a==b ?? (@p=> p==3) : (@p=> p==4)
are acceptable.
The pair operator :
makes a key/value pair which can be used
in a list to initialize a map.
Assignment is a statement, not an expression. Therefore, expressions like
a = b = 1
and while (a = count) < 20
are not allowed. In the latter
case, use while count@a < 20
. Comma is also not an expression and may
only be used where they are expected, such as a function call or lambda.
+
, -
, *
, /
, %
, and in
are the only operators that may be individually
overloaded. The ==
and !=
operator may be overloaded together by implementing
fun _opEq(a myType, b myType) bool
. All six comparison operators,
==
, !=
, <
, <=
, ==
, !=
, >=
, and >
can be implemented with just
one function: fun _opCmp(a myType, b myType) int
. If both comparison functions
are defined, _opEq
is used for equality comparisons, and _opCmp
is used
for the others. TBD: _opCmpOrdered
vs _opCmp
for unordered?
Like Golang, semicolons are required between statements but they are inserted automatically at the end of lines based on the last non-comment token and the first token of the next line.
Unlike Golang and C#, compound statements (if
, else
, while
, for
, lambdas, etc.)
can accept multiple lines without needing braces. The indentation is checked to make
sure it matches the expected behavior.
- Indentation is four spaces per scope level. No tabs anywhere in the source code except within multi-line string literals
- One statement per line, unless it's a continuation line. It's a continuation line if:
- The end of the previous line is
[
,(
,,
, or=>
. - The line begins with an operator, including
]
,)
,,
,"
,and
,or
,in
,+
,.
,=
, etc.
- The end of the previous line is
- Compound statements (e.g.
if
,while
,for
, etc.) may use or omit curly braces, but the convention is to omit them.
The while
loop is the same as C#. There is no do
statement, but it is easy to make one using scope
.
The scope
statement creates a new scope:
scope
let file = File.open("My File")
doStuff(file)
// File variable is out of scope here
The scope
statement can be turned into a loop using the continue
statement:
scope
DoSomething()
if WeWantToRepeat()
continue
Likewise, break
can be used to exit the scope early.
For the time being, for
loops only allow one format: for newVariable in expression
.
The simplest form of the for loop is when the expression evaluates to an integer:
// Print the numbers 0 to 9
for i in 10
Log.info("{i}")
// Print numbers from 1 to 10
for i in 1..+10
Log.info("{i}")
// Increment all the numbers in a list
for i in list.len
list[i] += 1
// Log key value pairs of all elements in a map
for kv in map
Log.info("Key: {kv.key} is {kv.value}")
When iterating over a collection, just like in C#, it is illegal to add or remove elements from the collection.
Both switch
and match
are reserved for future use. For now, use if
,
elif
, and else
to simulate them:
if myNum < 1
DoStuff()
DoOtherStuff()
elif myNum in 1..3
DoMoreStuff()
else myNum >= 3
DoTheLastThing()
A package is like a C# assembly. It is the basic unit for distributing
a library or application and is a .zip
file with a .zil
extension.
It will be defined here ZIL Specification.
Modules are like a C# static class and namespace combined. They can contain static functions, fields, and extension methods. From within a package, module names act like namespaces and stitch together just as they do in C#. From outside the package, they look and act like a C# static class.
The mod
keyword does not nest, or require curly braces. The module name
must be declared at the top of each file, after use
statements, and before
type, function, or field definitions. A file may contain other modules, but
all of them must be nested inside the top level module:
mod MyCompany.MyProject // Top level module
mod MyCompany.MyProject.Utils // Ok since it is nested in the top level
mod MyCompany.MyProject.OtherUtils // Ok since it is also nested
mod MyCompany.MyOtherProject // ILLEGAL since it is not nested
Package names should be unique across the world, such as a domain name
followed by a project (e.g. com.gosub.zurfur
). For now, top level module
names must be unique across an entire project. If there are any top level
module name clashes, the project will fail to build. In the future, there
may be syntax or project settings to resolve that.