diff options
| author | mryouse | 2022-07-30 01:02:43 +0000 |
|---|---|---|
| committer | mryouse | 2022-07-30 01:02:43 +0000 |
| commit | 1e2e0a307ee515ddf4a6de4012c02f0e8ef6d3f4 (patch) | |
| tree | 22873fbd82ee7539c9c19cafaea7f08303502b6e | |
| parent | 85298ce8bf9ce54da5d0918c9267e59431acd3ee (diff) | |
flesh out README
| -rw-r--r-- | README.md | 360 |
1 files changed, 216 insertions, 144 deletions
@@ -1,152 +1,224 @@ # neb ### an attempt at a language -## ideas - - **Lisp-y**: I hope you like parentheses! - - **Strongly typed**: types are Good, and could enable future compilation - - **We <3 Linux**: strong support for pipelines and shell-ing out - - **Immutable variables**: mutability is scary and makes for strange bugs - - **Pure functions**: side effects are also scary - -## things that (hopefully) work - -### housekeeping - - TODO `(exit [[status :int]]) => :bool` - -### io - - `(input [prompt :string]) => :string` - - `(print [out :string]) => :bool ; print to stdout` - -### file system - - `(exists? [filename :string]) => :bool` - - `(glob [regex :string]) => :list` - - `(read-lines [filename :string]) => :list` - - `(unlink [filename :string]) => :list ; returns empty list on success` - - `(with-write [filename :string] [[many :expr]]) => :any ; close file after block ends` - - `(write [content :string] [file :handle(?)]) => :list ; not yet sure how to handle, uh, handles` - -### comparison and boolean - - `(and [arg :bool] [many :bool]) => :bool` - - `(or [arg :bool] [many :bool]) => :bool` - - `(eq? [arg1 :literal] [arg2 :literal]) => :bool` - - `(> [left :number] [right :number]) => :bool` - - `(>= [left :number] [right :number]) => :bool` - - `(< [left :number] [right :number]) => :bool` - - `(<= [left :number] [right :number]) => :bool` - - `(not [arg :bool]) => :bool` - -### math - - `(+ [arg :number] [many :number]) => :number` - - `(- [arg :number] [many :number]) => :number` - - `(* [arg :number] [many :number]) => :number` - - `(/ [num :number] [denom :number]) => :number` - -### string - - `(first-char [arg :string]) => :string` - - `(concat [arg :string] [many :string]) => :string` - - `(join [arg :list] [join-val :string]) => :string` - - `(newline) => :string` - - `(rest-char [arg :string]) => :string` - - `(split [arg :string] [split-val :string]) => :list` - - `(strip [arg :string]) => :string` - -### flow control - - `(if [cond :bool] [t-branch :any|:expr] [[f-branch :any|:expr]]) => :any` - - `(for-count [count :int] [many :expr]) => :any ; creates 'idx' variable with loop count` - - `(for-each [items :list] [many :expr]) => :any ; creates '_item_' variable with current item` - - `(| [first :expr] [many :expr]) => :any ; creates 'items' variable` - - `(branch ([cond1 :bool] [expr1 :any]) [([condN: :bool] [exprN :any])]) => :any` - - `(block [expr1 :any] ... [exprN :any]) => :any` - - `(while [cond :bool] [many :expr]) => :any` - -### type checking - - TODO `(string? [arg :any]) => :bool` - - TODO `(int? [arg :any]) => :bool` - - TODO `(float? [arg :any]) => :bool` - - TODO `(number? [arg :any]) => :bool ; returns #true for :int or :float` - - TODO `(bool? [arg :any]) => :bool` - - `(list? [arg :any]) => :bool` - -### type conversion - - TODO `(int->string [arg :int]) => :string` - - TODO `(float->string [arg :float]) => :string` - - TODO `(number->string [arg :number]) => :string` - - TODO `(bool->string [arg :bool]) => :string` - - `(->string [arg :literal]) => :string ; works for all literals` - - `(string->int [arg :string]) => :int` +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 +```shell +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: +```scheme +(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 - - `(def [name :string] [value :expr]) => ? ; lexically scoped` - - `(redef [name :string] [value :expr]) => ? ; must already be defined` - -### shell - - `($ [command :string]) => :list ; doesn't support pipes, first item is return code` - - TODO `($| [commands :list]) => :bool ; pipes together multiple shell commands` - -### functions - - `(lambda (args) (expr1) (expr2) ... (exprN)) => :any` - - `(func name (args) (expr1) (expr2) ... (exprN)) => :any` - -### lists - - `(append [arg :list] [value :any]) => :list` - - `(empty? [arg :list]) => :bool` - - `(first [arg :list]) => :any ; car` - - `(in? [candidate :literal] [target :list]) => :bool` - - `(last [arg :list]) => :any` - - `(list [arg :any] [many :any]) => :list` - - `(list-length [arg :list]) => :int` - - `(list-reverse [arg :list]) => :int` - - `(remove [arg :list] [key :literal]) => :list ; this is actually for records` - - `(rest [arg :list]) => :any ; cdr` - - `(shuf [arg :list]) => :list ; shuffle the elements of the list` - - `(slice [arg :list] [start-idx :int] [[length :int]]) => :list` - - `(zip [arg1 :list] [arg2 :list]) => :list` - -### "higher order" - - `(apply [func :symbol] [target :list]) => :any` - - `(map [func :symbol] [target :list]) => :list` - -### other - - pretty much nothing else - -## TODO (this section may be incomplete) - -### types -- [ ] revisit type system when i understand *gestures broadly* - -### parsing -- [x] build an AST like a real person (sorta?) - -### math -- [x] division (float) -- [x] division (int) -- [ ] mod (int) -- [ ] exponent - -### strings -- [x] concat -- [ ] substring -- [ ] lower/uppercase - -### flow control -- [x] if -- [x] if with empty else -- [x] branch -- [x] pipe +You can initiate variables using the `def` keyword: +```scheme +(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. +```scheme +(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: +```scheme +(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: +```scheme +(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: +```scheme +(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 -- [x] lex -- [x] parse -- [x] evaluate - -### other structure things -- [ ] generic struct? -- [ ] dict? +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}`. -### symbols -- [x] define -- [x] access - -### shell -- [x] call out -- [x] pipe +### 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 |
