Skip to content

gavr123456789/Niva

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Niva is a simple language that takes a lot of inspiration from Smalltalk. But leaning towards the functional side and static types. Everything is still an object, but instead of classes, interfaces, inheritance, and abstract classes, we have tagged unions, which is the only way to achieve polymorphism.

In essence, niva is highly minimalistic, since its ancestor is Smalltalk. It introduces types, unions, and associated methods. Yes there are no functions.

On an imaginary complexity graph I would place the field here: Go < Niva < Java < Kotlin < Scala

Everything in niva is a message for some receiver kinda like eveything is an S-Expression in Lisp (+ 1 2) but in niva there are 3 types of expressions:

1 + 2 // binary - 2 args
42 factorial // unary - 1 args(receiver)
"foo-bar" split: "-" // keywoard - n args

Code Examples

Hello World
"Hello World" echo
Methods
type Person
  name: String
  age: Int

Person greet = "Hello, my name is " + name

// create person obj
p = Person name: "Alice" age: 24
// call greet and print
p greet echo

Message declaration

Some string methods
// unary
"foo-bar" count
"drawer" reversed
"BAOBAB" lowercase
// binary
"ee" == "ee"
"foo" + "bar"
// keyword
"foo-bar" split: "-"
"abcdef" forEach: [char -> char echo]
"baobab" filter: [it == 'b'] // it - implicit param
"foo-bar-baz" replace: "-" with: " "
"chocolate" contains: "late"

String STD

Some collections methods
// commas are optional, same as in Clojure
list = {1 2 3 4}
map = #{1 "one", 2 "two"}
set = #(1 1 2 2)

// {"2!" "4!"}
list
  filter: [it % 2 == 0],
  map: [it toString + "!"]

// {{1 2} {3 4}}
list chunked: 2

//Pair({3 4} {1 2})
list partition: [it > 2]
// splits into 2 collections, where condition is true and where its false
Control flow
// if - is a Bool Method that takes 2 lambdas arguments
// can be used as expression
x = 1 > 2 ifTrue: ["what?!"] ifFalse: ["yea"]
// newlines can be anywhere
{1 2 3} isEmpty
  ifTrue: []
  ifFalse: []

// blocks can have multiple statements
x count == 3 ifTrue: [
    "yea" echo
    x echo
]

name = "Alice"
// switch on name
| name
| "Bob"   => "Hi Bob!" echo
| "Alice" => "Hi Alice!" echo
|=> "Hi guest" echo

// you can switch on bool (and all primitive types)
| 1 > 2
| true => "what?!" echo
| false => "yea" echo

// if switch is used on Bool|Nullable|union type then its exhaustive, see Unions

Control flow

Simple algoritms

Fibonacci

Int fib -> Int = |this
| 0 => 1
| 1 => 1
|=> (this - 2) fib + (this - 1) fib

6 fib echo

Bottles of beer

Int bottles = | this
| 0 => "no more bottles"
| 1 => "1 bottle"
|=> "$this bottles"

onTheWall = " of beer on the wall, "
99 downTo: 1 do: [
  it bottles + onTheWall + it bottles + " of beer.", echo
  "Take one down and pass it around, " + it dec bottles + onTheWall, echo
]
"No more bottles of beer on the wall, no more bottles of beer." echo
"Go to the store and buy some more, 99 bottles of beer on the wall." echo
Code blocks
// A code block is a collection of statements that returns last expr
[
  x = 1
  y = 2
  x + y
]
// to run block send `do` message
x = [1 + 2]
x do echo // 3

[x * 2] // capture

// to run block with args send their names
add2nums = [a::Int, b::Int -> a + b]
result = add2nums a: 21 b: 21 // 42
Unions
union Color = Red | Blue | Green

// branches can have fields
union Shape =
| Rectangle width: Int height: Int
| Circle    radius: Double

constructor Double pi = 3.14
Double square = this * this

// match on this(Shape)
Shape getArea -> Double =
| this
| Rectangle => width * height, toDouble
| Circle => Double pi * radius square

// a * b, c is syndax sugar for (a * b) c


// There is exhaustiveness checking, so when you add a new branch
// all the matches will become errors until all cases processed

Shape getArea -> Double = | this
    | Rectangle => width * height, toDouble
// ERROR: Not all possible variants have been checked (Circle)
Bind JVM
// this is a message `package:content:` for Bind object
Bind package: "java.io" content: [
  type File path: String
  File readText -> String
  File writeText::String -> Unit
  File exists -> Boolean
]
// use
content = File path: "main.niva", readText
File path: "newFile.txt", writeText


Bind package: "java.math" content: [
  type BigDecimal value: String
  BigDecimal + x::BigDecimal -> BigDecimal
  BigDecimal - x::BigDecimal -> BigDecimal
  BigDecimal * x::BigDecimal -> BigDecimal
  BigDecimal / x::BigDecimal -> BigDecimal
]

x = BigDecimal value: "123456789123435"
y = BigDecimal value: "123456789123435"
z = x + y
z echo // 246913578246870

Other bindings: https://github.com/gavr123456789/bazar/tree/main/Bindings

Features

  • Simple expression based syntax inspired by Smalltalk
  • Smalltalk like hierarchy: projects -> packages -> protocols -> methods
  • Everything is an object and every method has a receiver(this\self)
    • So methods can be defined for core types like Int or List(unlike go)
    • Methods are not tied to type declaration, you can define new method from different file(like go)
    • That means you can define 5 days that returns seconds, just like in Ruby, but in a statically typed lang
    • Smalltalk syntax is great for creating DSLs without any complicated macros or AST manipulations: 1d/12m/2028y, in Java-like lang it would look like: (1.d()) / (12.m()) / (2028.y())
    • Operators are nothing special — they are just methods, but you cannot define new kinds of operators(so + for Vectors is ok, but no UFO like <=> from functional langs)
  • Simple semantics - the whole lang is essentially types(type\enum\union) and methods for them.
    • type is sum of fields, enum is union of objects, union is union of types
  • No build system, project files etc, niva run to compile and run project.
  • IDE support - LSP with plugins for VSC and Zed (check some demos here)
  • No NPE, nullability works the same as in Kotlin\Swift\TS
  • Errors work like a middle ground between values and exceptions (think Nim or Roc effects). In any given scope, all possible errors form a union you can match against exhaustively. Error docs
  • JVM\Kotlin compatibility, easy lib bindings (File example)
  • Easy serialize any obj via Dynamic type, no Docs yet, but its like json for JS or EDN for Clojure, write Dynamic toJson method and you get toJson for every type(since every type can be converted to\from Dynamic)
  • Docgen and unit tests included
  • No imports until full type name + fields clash. Since every modern IDE adds imports for you I decided to make the same on the compiler level
Backend

Current backend is Kotlin, because you get 4 backends for free - JVM, Native, JS, Wasm, also ecosystem is rich. A lot of pet-project languages are translated into js, which is very high-level, so why not be translated into a real language. Different backends are planned for self hosted implementation

About syntax

If you are not familiar with Smalltalk, its like lisp - everything is an expression(ifs, cicles etc) but not S-expressions, a different one without parentheses.

For example, everything except the declaration is sending messages to objects(receivers). 1 + 2 is not a + operator, but a + Int message for Int receiver.

Java-like: 1.inc().echo() C-like: echo(inc(1)) Lisp: (echo (inc 1)) Niva: 1 inc echo

with args: Swift\Kotlin: person fooBar(foo = 1, bar = 2) Niva: person foo: 1 bar: 2 So names of the args and method signature are the same thing.

// declare type with 2 fields
type Person name: String age: Int
person = Person name: "Alice" age: 24 // instantiate

person name echo // get name and print it
personWithNewName = person name: "new name"

// unary method declaration
Person hi = "Hi! my name is $name" echo
person hi // unary call

// method with args
Person foo::Int bar::Int = [
  age + foo + bar, echo // same as
  (age + foo + bar) echo
]
person foo: 1 bar: 2 // 27 printed

union Shape =
| Rectangle width: Int height: Int
| Circle    radius: Int

constructor Float PI = 3.14
Float PI // constructor call

// fields can be placed on new lines
type Point
  x: Int
  y: Int

constructor Point new = Point
  x: 0
  y: 0

p = Point new

Project examples

Learn

Install

git clone https://github.com/gavr123456789/Niva.git
cd Niva/Niva
./gradlew buildJvmNiva # takes one min for the first time :(
# read the instructions from output on how to add compiler binary to PATH for each shell
# LSP here https://github.com/gavr123456789/niva-vscode-bundle

More in Installation

First program

// main.niva
"Hello World" echo

niva run Default entry point is always main.niva file.

Other commands: niva filename.niva to run file niva run to run all files in the folder recursivelly starting from main.niva niva build to output jar\binary file niva info > info.md to output all types and their methods niva --help for more

Project with 2 files

Try to create new file and put the foo:bar: method in it:

//foobar.niva
type FooBar
FooBar foo::Int bar::Int = [
  foo echo
  bar echo
  ^ foo + bar
]

in main.niva

// to create type without fields send `new`
fb = FooBar new
result = fb foo: 19 bar: 23
result echo

now lets rewrite it into single expr, just for fun

FooBar new foo: 19 bar: 23, echo

What that comma means? In niva there is only one precedence order: unary > binary > keyword Here a complicated (scary) example:

1 inc inc > 2 dec dec // 3 > 0 -> true
// adding keyword call
1 inc inc > 2 dec dec or: 3 dec < 4 inc

// lets apply () to see the order more clearly
((1 inc inc) > (2 dec dec)) or: ((3 dec) < (4 inc))

// another example
1 inc inc > 2 dec dec ifTrue: ["yay!!!" echo]
((1 inc inc) > (2 dec dec)) ifTrue: ["yay!!!" echo]


// but what if we want to send some unary message to result of a keyword
"foo-bar-baz" replace: "-" with: " " echo
// Here we made a mistake, echo will be sent to " ", not to result of `replace:with:`
// to fix that we can wrap the whole thing:
("foo-bar-baz" replace: "-" with: " ") echo
// or just put a comma
"foo-bar-baz" replace: "-" with: " ", echo

// same situation when we want to chain many keyword messages
"aabbcc"
  replace: "a" with: "d",
  replace: "b" with: "e",
  replace: "d" with: "f"

// list chain
1..10,
  toList
  filter: [it % 2 == 0],
  map: [it toString + "!"],
  forEach: [it echo]

Method with body

// square brackets gang
Int add4 = [
  x = this + 1
  add3 = x inc inc inc
  ^ add3 // ^ is return, thats from Smalltalk
]
1 add4 echo

Method with args(keyword)

type Person name: String
p = Person name: "Alice"
// the constructor call is a keyword message for the type itself

Person greet

Simple compile time reflection

Assert
TODO

Test included

Create a message for Test type anywhere to create test. Here you can find some lexer tests examples of NIN impl. https://github.com/gavr123456789/Niva/blob/main/Niva/NivaInNiva/front/lexer/lexTest.niva#L27

Test arithmetic = [
  actual = 2 + 2
  expected = 4
  (actual == expected) assert
]
// very simple assert implementation
Boolean assert =
  this == false ifTrue: [Error throwWithMessage: "assert failed", orPANIC]

niva test outputs: main > arithmetic ✅

Union

https://gavr123456789.github.io/niva-site/unions.html

// branches can have fields
union Shape =
| Rectangle width: Int height: Int
| Circle    radius: Double

constructor Double pi = 3.14
Double square = this * this

// match on this(Shape)
Shape getArea -> Double =
| this
| Rectangle => width * height, toDouble
| Circle => Double pi * radius square

// There is exhaustiveness checking, so when you add a new branch
// all the matches will become errors until all cases processed

Shape getArea -> Double = | this
| Rectangle => width * height, toDouble
// ERROR: Not all possible variants have been checked (Circle)

Bind Java std and read the file

Create io.simple.bind.niva in the same folder

// very simple text read write
Bind package: "java.io" content: [
  type File path: String
  File readText -> String
  File writeText::String -> Unit
  File exists -> Boolean
]

main.niva

// read all text from the file to String
File path: "main.niva", readText
File path: "newFile.txt", writeText: "Hello from niva!"

Name

So far I've chosen niva because my 2 favorite static languages are nim and vala.

Backend

Current backend is Kotlin, because you get 4 backends for free - JVM, Native, JS, Wasm, also ecosystem is rich. A lot of pet-project languages are translated into js, which is very high-level, so why not be translated into a real language.