|
8 months ago | |
---|---|---|
aoc/day01 | 10 months ago | |
cipher | 8 months ago | |
libs | 8 months ago | |
neb | 8 months ago | |
neighborcat | 9 months ago | |
rosetta | 8 months ago | |
tests | 8 months ago | |
.gitignore | 9 months ago | |
Makefile | 8 months ago | |
README.md | 8 months ago | |
commands.txt | 8 months ago | |
docs.neb | 8 months ago | |
interpreter.d | 8 months ago | |
neb.py | 8 months ago | |
repl.neb | 8 months ago |
README.md
neb
an attempt at a language
Neb is a gradually typed language with Lisp-like syntax, with strong influences from both Lisp-y languages and Python. It is a hobby language in the fullest sense of the word -- it is designed and used for the pure pleasure of desiging and using a programming language. It is a playground within which crazy ideas can be dreamt up and implemented.
high-level goals
- Readability: neb code should be straightforward to read, both in terms of
program clarity, but also in terms of the literal names of things. It should
ideally be self-evident what a function does by it's name alone (i.e. while neb
is in the Lisp family, it uses
first
andrest
instead ofcar
andcdr
) - Self-documenting: this dovetails off readability, but the neb interpreter should be able to tell the user how to call a function and what it should return, without any extra steps from the programmer.
- Interactive: neb has a REPL (as most languages do nowadays, and Lisps always have), but ideally the REPL should be able to function as a fully equipped neb programming environment, complete with documentation, tab completion, as well as additional niceties for both writing and inspecting neb code.
- Adaptable: neb should be easy and flexible enough to write one-off scripts, but also robust enough to support them in their transition from one-offs to small or medium sized programs.
install
git clone https://git.rawtext.club/neb/neb-python.git
cd neb
make
coding in neb
Hello World!
The print
function in neb takes in a single :string
as an argument, and prints
it to the screen:
(print "Hello, World!") ; prints "Hello, World!" to the screen
comments
Comments begin with the ;
character, and last until the end of the line. There
are no multi-line comments -- each line of the comment must begin with ;
.
variables
You can initiate variables using the def
keyword:
(def greeting "Hello") ; defines the symbol 'greeting'
(print greeting) ; prints "Hello" to the screen
Assign a new value to an existing variable using redef
. def
will attempt to
create a new variable with the desired name in the current lexical scope, whereas
redef
will update the value of a variable defined in the current lexical scope
or parent scopes.
(def greeting "Hello!")
greeting ; "Hello!"
(block
(def greeting "Goodbye!") ; 'greeting' is in the scope of 'block'
greeting) ; "Goodbye!"
greeting ; "Hello!" (unchanged)
(block
(redef greeting "See ya")
greeting) ; "See ya"
greeting ; "See ya"
expressions
Generally speaking, everything between ()
is an expression in neb, and expressions
all evaluate to a value. Expressions that are purely side-effect oriented, and thus
don't have a good "value" to return, may return :nil, which is equivalent to an empty
list. There is nothing that differentiates :nil from an empty list.
Some expressions may wrap multiple expressions, as block
does above. These expressions
evaluate to the value of their last evaluated expression.
scopes
Neb is lexically scoped, so variables refer to their innermost definition, and may
be shadowed. A scope can be manually created using the block
function. Some functions
automatically create blocks of scope:
func
andlambda
- loop constructs (
for-each
,while
) - the catch clause of the
try
expression (specifically for a magic variable, see below)
Note: statements that branch, such as if
and, uh, branch
, do not create their
own scope (at this time), as they expect only a single expression as their branch
clause. To evaluate multiple expressions within these constructs, use block
.
magic variables
Neb has a few magic variables to cut down on some code. These may not be around forever.
for-each
sets the magic variable _item_
, which is the current item in iteration:
(def list-of-strings (list "Hello" "there" "friends"))
(for-each list-of-strings
(print _item_))
; Hello
; there
; friends
for-count
sets the magic variable _idx_
, the index (one-based) of the iteration:
(def list-of-strings (list "Hello" "there" "friends"))
(for-count (length list-of-strings)
(print (->string _idx_)))
; 1
; 2
; 3
try
sets the magic variable _panic_
, available in the catch clause, which
contains a :string with the error message:
(def list-of-strings (list "Hello" "there" "friends"))
(try
(+ list-of-strings) ; error
(print _panic_)) ; '+' expects a :number, received :[:any] (got ...)
wait, rewind, one-based indexes?
Yep! It's not as much of an adjustment as you would think, especially since if you're using functional constructs, you generally won't care about the index of an element in a list. Zero-based is confusing, and exacerbates off-by-one errors, and is legacy we don't need to perpetuate. Plus, it's nice to start counting at 1.
Did I mention this is a space for crazy ideas?
builtin types
Types in neb begin with a :
character. There are many built-in types:
:any
: this is the type from which all other types descend. It matches values of all other types, including types.:literal
: this is the parent of all atomic types, including:string
: strings, enclosed with double quotes:bool
: boolean, either#true
or#false
:int
: integers:float
: floating point numbers, contains a period:number
: a super-type of both:int
and:float
:handle
: a file handle:type
: the type of types
lists
The basic collection type in neb is a list. A list can contain any number of any
type of item, including zero. The base type of all lists is :[:any]
. A list may
be defined to contain an inner type -- so, for example, something that conforms
to the type :[:string]
is a list of zero or more elements, each of which is a
:string
.
nil
The :nil
type is a subtype of :[:any]
, and the only thing that is of this type
is a list with exactly zero elements.
non-empty lists
The counterpart of :nil
is denoted using curly braces -- something that conforms
to the type :{:string}
is a list of at least one item, where every item is a
:string
. :[:any]
can be thought of as the type encompassing both :nil
and
:{:any}
.
functions
Functions are defined using either the lambda
or func
keywords -- func
specifies
a named function, and lambda
an anonymous one. Functions are defined as such:
(func <name> [<return type>] ([<arg> [<arg type>]]*) <expressions>)
This breaks down to:
- name: the name of the function
- return type: the return type of the function. This will be enforced in future versions of neb (i.e. if a function evaluates to something incompatable with the return type, an error will be thrown). This is optional.
- arg list: this is in opening and closing parentheses, even if there are zero arguments. Each argument must have a distinct name, and may optionally be followed by a type. If a type is included, the interpreter will ensure that the value being passed as that argument conforms to the specified type.
- expressions: one or more expressions to be evaluated. A new scope is created in which the arguments are defined.
overloading
Functions can be overloaded by both type and arity. For example, in the standard
library, the function reverse
is defined in two ways:
(reverse :[:any]) ; => :[:any]
(reverse :string) ; => :string
This is an example of overloading by type -- one implementation of reverse
works
on :[:any]
, and the other works on :string
. The interpreter evaluates the
argument passed in, and dispatches according to the type of the argument. If a
matching implementation is not found, an error is thrown.
Another example is exit
, defined as:
(exit) ; => :nil
(exit :int) ; => :nil
It will accept either zero or one argument, and dispatch accordingly.
Built-in functions can be overloaded by user-defined functions, and user-defined functions can be overloaded as well.
ambiguity
In the case where there are multiple defined functions that can satisfy the arguments passed, an error is thrown. An example of this:
(my-func :[:string]) ; => :[:string]
(my-func :[:number]) ; => :[:number]
my-func
above is ambiguously defined for the case of an empty list, because both
:[:string]
and :[:number]
are compatible with an empty list (note: this will
only throw an error when passed an empty list -- if a non-empty list is passed,
the function will succeed). To define this unambiguously, define it as such:
(my-func :{:string}) ; => :{:string}
(my-func :{:number}) ; => :{:number}
(my-func :nil) ; => :nil
Note: the return types in this example are unimportant.
truthiness
In neb, the only values that evaluate as either True or False are the :bool
values
#true
and #false
. So, anything that expects a :bool
will throw an error if
it receives anything else. I only call this out because many languages interpret
different values a "truthy" or "falsy", i.e. while(1)
being an infinite loop, as
1 evaluates "truthy".
things that don't work, but hopefully will eventually
- return type checking
- closures
- real namespaces
- recursively nested types
- docstrings for functions