a silly little language
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
mryouse ce15f14797 initial commit of (extremely) bare bones D implementation 2 months ago
aoc/day01 day1 part2 (without slices) 4 months ago
cipher deprecate with-write and list-length 2 months ago
libs tighter definition 2 months ago
neb implement slice for strings 2 months ago
neighborcat remove 'with-write' blocks, those will come with macros(?) 3 months ago
rosetta exercises 2 months ago
tests s/:list/:[:any]/ and make leading dot illegal 2 months ago
.gitignore ignore pycache in subdirectories 3 months ago
Makefile initial commit of basic Makefile 2 months ago
README.md bugfix: markdown formatting 2 months ago
commands.txt update commands to reflect :nil 2 months ago
docs.neb update for new commands 2 months ago
interpreter.d initial commit of (extremely) bare bones D implementation 2 months ago
neb.py add support for init.neb 2 months ago
repl.neb WIP repl still doesn't really work 2 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 and rest instead of car and cdr)
  • 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 and lambda
  • 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