# Sphinx syntax examples
# If you use Sublime Text you can read this with syntax highlighting!
# (highlighter for sublime text provided by Sphinx.sublime-syntax)
#{
Block Comment
#{ Nesting! }#
}#
# Your first Sphinx program!
print("Hello, world!")
# Semicolons are optional. The syntax has been designed so that the end of a statement can always be inferred.
"One Stmt"; "Two Stmt"
# Like Lua, the syntax has been chosen to ensure that
# the end of a statement can always be inferred without
# needing to be whitespace-sensitive.
# The language will support type annotations and have structural type inference built in
# Any expression can be annotated, though outside of certain positions you may need parentheses
# This is still WIP
("Type Annotations" : String)
# Booleans
true # Not false.
false # Not *not* false.
# Numbers
1234 # An integer.
12.34 # A decimal number.
0xFACE # Hexadecimal integer.
# Strings
"I am a string"
"" # The empty string.
"123" # self is a string, not a number.
'And then she said, "woah."' # Can be single or double quoted.
"\ttab\nnewline, literal\\backslash" # Escape sequences
r"/[\w._%+-]+@[\w.-]+\.[a-zA-Z]{2,4}/" # Literal with escape sequences disabled
let two = 2
$"numbers: 1 {two} 3 {2*2}" # interpolated string
# Nil
nil
# Tuple
"abc", 123
# parens are only required for these special cases
() # the empty tuple
("solo",) # tuple with one element
# Objects
{ value = 0xA } # anonymous object
Person { value = 0xC } # classy object
get_person_type() { name = "Bob", age = 23 } # type can be an expression
{ name: String = "Bob", age: Int = 23 } # with annotations
{ property = "Books Read", var value = 13 } # with mutable field
# Arithmetic expressions
add + me
subtract - me
multiply * me
divide / me
modulo % me
# (x^2) + (y^2) # TODO exponentiation?
# Bitwise and shifts
(17 ^ 73) << 3
2412 & 0xff | ~0xcc00
# unary
-negate_me
~invert
# Comparison and equality
less < than
less_than <= or_equal
greater > than
greater_than >= or_equal
1 == 2 # false.
"cat" != "dog" # true.
314 == "pi" # false.
123 == "123" # false.
# booleans are not implicitly integers (but all expressions do have a "truth" value)
true == 1 # false
true == bool(1) # true
# Logical operators
not true # false.
not false # true.
# these will short circuit
true and false # false.
true and true # true.
false or false # false.
true or false # true.
# Precendence and grouping
average = (min + max) / 2
# Blocks
begin
print("One statement.")
print("Two statements.")
"Result value."
end
begin
if condition then
break "Early exit" # can break out of blocks early if you want
end
"Fallback value."
end
# Variable declaration and assignment
# Declarations are used to create new variables.
# They are written just like assignments except the
# variable name expression (lvalue) is preceeded by
# either "let" or "var":
let im_a_variable = "here is my value" # immutable variable
let i_am_nil = nil # initializer is required
var i_can_be_mutated = "initial_value"
i_can_be_mutated = "another_value"
let im_a_float: Float = 3.14159 # with type annotation
# destructure tuples
var first, second = "abc", "def"
second, first = first, second
# destructuring assignment with type annotations
let (index: Int, val: String) = 0, "example"
# assignment expressions are expressions, and so can be chained
var b = 0
var c = 1
let a = b += c = let d = 1 # chaining assignment (right associative)
let a = (b += (c = (let d = 1))) # previous expression is equivalent to this
# Normally, the let/var keywords apply to the entire left-hand side of the assignment
# If you want to apply a different keyword to an inner item, you can surround it in parens
let a, b, (var c) = "a", "b", "c"
# another example combined with tuple destructuring
a, (let b, (var c), d), e, (let f), (var g) = "a", ("b", "c", "d"), "e", "f", "g"
# local assignment is the default, but if you are using let/var/nonlocal for a
# destructuring assignment and need to do a local assignment somewhere in there, you can
let a, b, (c, d, (local e), f) = get_tuple()
# assignment versions of all binary operators are supported for non-tuple assignment
inc = 0
inc -= 1 # update-assignment
# variables can be freely redeclared (shadowed)
let breakfast = "bagels"
print(breakfast) # "bagels".
var breakfast = breakfast
breakfast = "beignets"
print(breakfast) # "beignets".
del a # drop a variable from the current scope
del collection[key] # mostly useful for removing values from objects
# assigment expressions bind weaker than operator expressions
3 * b += c # Error, assignment target is not an lvalue
3 * (b += c) # Ok
# Control Flow
# branching constructs are expressions
# looping constructs are statements
cond_expr =
if condition1 then
"first"
elif condition2 then
"second"
else
"default"
end
# If no branch is entered in an if-expression (which can happen if there is no "else")
# the whole expression evaluates to the last value that was tested. So for example:
# If-expressions evaluate to the last item of the branch that they enter
if (if a then true end) then
print("if-ception")
end
# If-expressions evaluate to their condition if they don't enter a branch
if (if false then true end) then
assert false
end
# loops support both break and continue statements
var a = 1
while a < 10 do
if exitcond then
break
end
if skipcond then
continue
end
print(a)
a += 1
end
# break and continue can use labels to target an outer loop
::outer while example_cond() do
some_work()
while inner_cond() do
if exit_inner() then
break
end
if continue_outer() then
continue ::outer
end
if exit_outer() then
break ::outer
end
end
end
loop
var cond = example()
if not cond then
break
end
end
# for loops using iterators
# here, create_iter() returns an "iterator"
for item in create_iter() do
print(item)
end
# Functions
make_breakfast(bacon, eggs, toast)
make_breakfast()
# implicitly returns nil
fun echo_sum(a, b)
print(a + b)
end
# function parameters can be let or var, just like other variables (let can be omitted)
fun mutable_params(a, var b)
b = a
end
# note: unlike Python, default arguments are re-evaluated with each invocation
fun echo_default(thing = "default")
print(thing)
end
# optional type annotations
fun return_sum(a: Float, b: Float = 0.0) -> Float:
return a + b
end
# Closures
fun add_pair(a, b)
return a + b
end
fun identity(a)
return a
end
print(identity(add_pair)(1, 2)) # echos "3".
fun outer_function()
fun local_function()
print("I'm local!")
end
local_function()
end
fun return_functions()
var outside = "outside"
fun print_value()
print(outside)
end
fun hide_outside()
let outside = "local"
print(outside)
end
fun set_outside()
outside = "inside"
end
return print_value, hide_outside, set_outside
end
let print, hide, set = return_functions()
print() # prints "outside"
hide() # prints "local"
print() # prints "outside"
set()
print() # prints "inside"
let anon_func = fun(x) x + 1 end
let a = anon_func(3)
# Decorator Syntax (Not implemented yet)
# Using "@" syntax you can apply a function to any value being assigned in a declaration, essentially:
@decorator let name = value
# becomes:
let name = decorator(value)
# Since named function and class definitions are just syntactic sugar over a variable declaration,
# You can use this to wrap functions and classes like you might see in other languages like Python.
fun trace(wrapped)
return fun(args...)
print("trace")
wrapped(args...)
end
end
@trace
fun increment(x)
return x + 1
end
# The decorator can be any expression that evaluates to a callable, as long as the callable takes a single argument:
@some["strange"].decorator(true)
fun example() end
# A contrived example to show that the mechanism is not specific to functions.
fun super_int(n) return 9001 * n end
@super_int
let super_seven = 7
print(super_seven) # 63,007
# It could even work with regular assignment, although the usefulness of this is dubious
# I'm debating whether or not this should even be allowed.
@super_int
super_seven /= 1 # makes it un-super thanks to division
print(super_seven) # 7
# Errors
#{
One of the goals I have for Sphinx is to not use exceptions for control flow in the way
that Python does, where try/catch constructs are used to branch based on whether an
operation succeeded or not.
The default is to fail fast. If you do something that causes an error (and it wasn't intentional),
the program exits with a traceback. There will be a Lua-style "protected call" mechanism because
that is useful for top-level error handling, but it won't be the primary error handling mechanism.
Instead, when you are expecting an expression to cause an error, you can apply the "try operator"
to the expression. If the expression fails, the try operator instead forces it to evaluate to
an error object. This is similar to how Lua uses nil to indicate that a value doesn't exist in
a table, but unlike nil, the error object contains within itself a complete traceback and other
information that describes the error and the place where it originated.
This allows you to capture detailed error information similar to what Python provides with
exceptions, without causing the control flow jumps that sometimes draws criticism of try/catch
error handling. Instead you get an error value, and control flow proceeds normally.
Also, since receiving an error value is explicitly "opt-in" using the try operator, you control
exactly where an error value might be generated and you know where to expect them.
Once you have an error value, attempting to perform operations on an error value propagates the
error instead of failing fast, preserving the original error info and traceback.
While this is a departure from the explicit opt-in, it requires you to explicitly opt-in
somewhere first, and the error object will always preserve the source of the error so that it
can be traced.
}#
# TODO examples
# Classes
something.field = 1
something.method()
class Breakfast
# declare class-level variables
var classvar = 0
# type annotate possible member variables
var self.field # member named "field" is mutable
let self.name: String # member named "name" is immutable and has type "String"
# functions whose first parameter is the "self" symbol
# are automatically converted into builtin method descripter objects
fun cook(self)
print("Eggs a-fryin'!")
end
fun mutates_self(var self, value)
self.field = value
end
# even when created using an anonymous function
var serve = fun(self, who)
print("Enjoy your breakfast, " + who + ".")
end
# use classmethods to define constructor functions
fun new(name)
return Breakfast { field = 1, name = "Eggs and Bacon" }
end
end
# Maybe. TBD
Breakfast.cook = fun(self)
print("Eggs a-fryin'!")
end
# Store it in variables.
let someVariable = Breakfast
# Pass it to functions.
someFunction(Breakfast)
let breakfast = Breakfast {} # object initializer
print(breakfast) # "Breakfast instance".
# Instantiation and initialization
# originally initializers used { name: value }, but I want to reserve colons for future type hinting
var breakfast = Breakfast { meat = "sausage", bread = "sourdough" }
breakfast.drink = "coffee"
del breakfast.bread # remove an object member
let a = 0
let anonymous = { some = a = 1, data = 2 }
class Breakfast
fun serve(self, who)
print("Enjoy your " + self.meat + " and " +)
self.bread + ", " + who + "."
end
# ...
end
class Breakfast
# constructor function
fun new(meat, bread)
return Breakfast { meat = meat, bread = bread }
end
end
var baconAndToast = Breakfast.new("bacon", "toast")
baconAndToast.serve("Dear Reader")
# "Enjoy your bacon and toast, Dear Reader."
# Inheritance
class Brunch: Breakfast
fun drink()
print("How about a Bloody Mary?")
end
end
# Variadic arguments
fun variadic_fun(normal_arg, variadic...)
print(normal_arg)
for variadic_arg in variadic do
print(variadic_arg)
end
end
fun variadic_default(sequence... = ())
for item in sequence do
print(item)
end
end
variadic_fun("red", "blue", "green") # prints "red" then "blue" then "green"
var mylist = list.new("red", "green", "blue") # important use case: constructor functions for collection types
# Argument unpacking
var things = ("second", "third")
variadic_fun("first", things...) # prints "first" then "second"
variadic_fun(things...) # prints "second" then "third"
# named arguments are not supported
# instead, use a function that takes an object as a single argument
# and use an anonymous object literal
lotta_options({ tag = "example", option1 = true, option2 = 3 })
# Metamethods
class Example
fun __setindex(self, index)
print(index)
end
fun __call(self, args...)
print(args)
end
var __eq = fun(self, other) true end
end
var ex = Example { [0] = "first", [1] = "second" }
ex[2] = "third"
# List of allowed metamethods:
#
# __add, __mul, __sub, __div, __neg, etc...
# __le, __lt, __eq only, no methamethods for others...
# __getindex, __setindex, __delindex
# __bool, __tostring
# __exit - for context managers
# __call
# class/instance names with double underscores are reserved for metamethods
class Example
var __notallowed = true # error
fun __alsonotallowed(self)
print("bad")
end
end
Example.__fail = -3 # this will result in an error
# Multiple inheritance
class A
fun method(self)
print("A.method() called")
end
end
class B
fun method(self)
print("B.method() called")
end
end
class C: A, B end
class D: C, B end
var d = D {}
d.method() # prints "A.method() called"
# see also: C3 Linearization