It's Smalltalk like language, but statically typed.
So far I've chosen niva because my 2 favorite static languages are nim and vala.
Peek_2024-02-22_02-09.mp4
Learn language here: https://gavr123456789.github.io/niva-site
Examples folder is here
And in thisrepo you can find WIP implementation of interpreter from the book https://interpreterbook.com/
- simplicity - its types, tagged unions and methods for them
- (OOP - inheritance - late bindings) + some FP(unions, matching, immutability by default)
- kinda scripting langs experience, this is the whole helloworld program
"Hello World!" echo
- Smalltalk Syntax
- Inline REPL - new way of print debugging ^_^
- Imports inference - while types have no identical names and set of fields, u dont need to write imports!
- JVM/Kotlin interop
- Smalltalk syntax
- Editor support with LSP and vscode or zed(not public yet) plugin
- Smalltalk syntax(Iloveit)
- Errors are effects, catching is pattern matching on possible errors of the call. You can ignore catching, but then you need to put them to return type signature
-> Int!IOError
or-> Int!
- No NPE, nullability works the same as in Kotlin\Swift
Errors as effects, 3 kind of msg to handle them
https://github.com/user-attachments/assets/08ce8d75-e5e7-478c-a5b6-54825d7a0a48
Tested on Linux Arch\Nix, Mac M1 and Window$, both compiler and LSP server.
Warning: the first compilation of "Hello world" niva program can take time because of gradle deps.
You can choose jar or native version, jar is much easier to get, native is ~300ms faster on m1.
Run in Niva/Niva folder:
./gradlew buildJvmNiva
- will create jvm based binary in ~/.niva/niva/bin, u can add it in path
JVM > 22 is required, Im using https://www.graalvm.org/downloads/
GraalVM binary compiles hello world 300ms faster on M1(70% of the time takes gradle anyway)
If you have graalvm in your JAVA_HOME then run:
./gradlew buildNativeNiva
this will create native binary in ~/.niva/bin
Arch: yay -S jdk22-graalvm-bin
archlinux-java status
archlinux-java set <JAVA_ENV_NAME>
select java on arch
macOS: brew install --cask graalvm-jdk
/usr/libexec/java_home -V
export JAVA_HOME='/usr/libexec/java_home -v 22.0.2'
select java on mac os
If you have expanded from macro 'NS_FORMAT_ARGUMENT'
problem with buildNativeNiva on macOS then update XCode
xcode-select -p && sudo xcode-select --switch /Library/Developer/CommandLineTools
niva filename.niva
to run file
niva run
to run all files in the folder recursivelly starting from main.niva
niva run filename.niva
same but different entry point
niva build
to output jar\binary file
niva info > info.md
to output all types and their method signature
dont use run
inside niva repo, since niva has no build system or project files it just collect all the files recursively, so it will try to compile all the examples.
use --verbose
flag to mesure time of compilation steps.
Is here https://gavr123456789.github.io/niva-site
- LSP Server
- VS Code extension full lsp support with autocompletion, error highlighting, goto definitions
To get started with Niva, use the provided shell.nix file.
Enter the following command: nix-shell
This command will set up the necessary dependencies and run the compile script to produce a binary file.
Afterwards, you can run the Niva compiler with the following command:
./niva_compiler/niva <file>
Can be outdated, read https://gavr123456789.github.io/niva-site
Almost everything in this lang is message send(function call), because of that there are 3 ways of doing it(don't worry, none of them requires parentheses).
Everything is sending a message to an object.
There no such things as function(arg)
, only object message
"Hello world" echo // print is a message for String
You can send as many messages as you want
"12.08.2009" asDate days echo // not real, just syntax example
Okay, but what if the message has some arguments?
Just separate them with colons, this is called a keyword message:
obj message: argument
1 to: 5 // oh, we just created nice range syntax
And what about many arguments?
Easy
1 to: 5 do: [ it echo ] // 1 2 3 4 5
aand we don't need things like hardcoded for/while/do_while loops in language anymore, the second argument here is a code block.
Here some more examples:
5 factorial // unary
5 + 5 // binary (only math symbols allowed)
map at: "key" put: "value" // keyword
1..5 forEach: [it echo] // all together + codeblock
Niva is statically typed language, so we need a way to declare custom types, here it is, just the syntax of keyword messages with type keyword.
Each type automatically has a constructor that represents as the same keyword message, isn't it beautiful?
// Declare type Person with 2 fields and create obj
type Person name: String age: Int
person = Person name: "Bob" age: 42
To declare method for type just type Type function_signature = body
// unary method declaration for Person receiver
Person sleep = [...]
// method call
person sleep
// with arguments(keyword message)
TimeInterval from: x::Date to: y::Date = [
// using x and y
]
In the last example you can see a problem, we dont need local names for from and to. Because of that there are another way to declare keyword message:
Random from::Int to::Int = ... // `from` and `to` are local arguments
All methods are extension methods(like in Kotlin or C#), so you can add new methods for default types like Int
You can also define many methods at once
extend Int [
on increment = this + 1
on decrement = this - 1
]
// instead of
Int increment = this + 1
Int decrement = this - 1
1 < 2 ifTrue: ["yes!" echo]
1 > 2 ifFalse: ["no!" echo]
// `ifTrue:ifFalse:` is single keyword message with 2 arguments and boolean receiver
1 > 2 ifTrue: [...] ifFalse: [...]
// ifTrue:ifFalse: can be used as expression
x = 42 < 69 ifTrue: [1] ifFalse: [0] // x == 1
There are no special syntax for loops, only messages
// Loops
// collections have forEach method that takes block
{1 2 3} forEach: [it echo] // 1 2 3
{1 2 3} forEachIndexed: [i, it -> i echo] // 0 1 2
// IntRange
1..3 forEach: [it echo] // 1 2 3
1..<3 forEach: [it echo] // 1 2
// single expression
1 < 2 => "yes!" echo
// with body
1 < 2 => [
...
]
// with else
1 > 2 => "yes" echo |=> "no" echo
// as expression
x = 1 > 2 => "yes" |=> "no" // x == "no"
// multiline switch
x = 1
| x
| 1 => "switch 1" echo
| 2 => "switch 2" echo
|=> "what?" echo
// single line switch
m = |x| 1 => x inc | 2 => x dec |=> 0
// m == 2 because of x inc
There is no elseIf syntax but you can chain simple then-else
x = 1
y = x > 2 => ">2" |=>
x < 2 => "<2" |=>
"???"
// y == "<2"
factorial
is a message for Int
type, that returns Int
.
self
is context-dependent variable that represents Int on which factorial is called.
The whole function is one expression, we pattern matching on self with |
operator that acts as swith expression here.
// switching on this(Int receiver)
Int factorial -> Int = | this
| 0 => 1
|=> (this - 1) factorial * this
5 factorial echo
Int isEven =
this % 2 == 0 => true
|=> false
5 isEven echo
4 isEven echo
There 3 types
5 factorial //unary
5 + 5 // binary (only math symbols allowed)
map at: "key" put: "value" // keyword
This feature is directly from smalltalk. Its the same as Clojure doto or Pascal With Do or Kotlin with, many langs has something like that:
;
is cascade operator, that mean the message will be send to the first receiver, not to the result of the previous one
c = {}. // empty list
c add: 1; add: 2; add: 3
is the same as
c = {}.
c add: 1
c add: 2
c add: 3
That is, imagine that you have an add function that takes 2 numbers and returns their sum C lang
int add(int a, int b) {
return a + b;
}
If you chain functions like this
add(1, add(2, add(3, 4)))
you will get 1 + 2 + 3 + 4
and if you wanna apply all functions to the same value you will need:
int a = 0;
a = add(3, 4);
a = add(a, 2);
a = add(a, 1);
Lest imagine its Java, and int itself has add method then:
int a = 0;
a = a.add(1).add(2).add(3).add(4);
will give you 1, because each call after add(1)
was applied to the result of the previous call, and not to the original variable a.
So to get fluent desing in Java like languages you need to return this or new instance of this on each method.
So the same on niva will looks like:
Int add: b -> Int = [
self + b
]
a = 0 add: 1; add: 2; add: 3; add 4 // 10
This is very convenient for chaining any calls. Since setting field values is also a message, you can do this:
person = Person name: "Alice" age: 42
person name: "Bob"; age: 43 // send message name and age to person
boxWidget = Box width: 40 height: 50
boxWidget
add: Label text: "hello";
add: Button text: "press me";
height: 100;
You can create code block like this:
block = ["hello from block" print]
To evaluate block send value message to it:
block value // hello from block printed
Block with arguments:
block::[Int -> Int] = [x -> x + 1]
block value: 1 // 2
Many args -> many value messages:
block::[Int, Int, Int -> Int] = [x y z-> x + y + z]
block value: 1 value: 2 value: 3 // 6
If you have a better ideas how to send many arguments to blocks please make an issue. I have anoter variant in mind with named args
block::[Int -> Int] = [it + 1]
block it: 1 // 2
block x: 1 y: 2 z: 3 // 6
Declaration:
union Shape area: Int =
| Rectangle => width: Int height: Int
| Circle => radius: Int
rectangle = Rectangle area: 42 width: 7 height: 6
rectangle area echo
rectangle width echo
rectangle height echo
Both Rectangle and Circle has area field. Every branch is usual type, so you can create them separately.
Rectangle area: 42 width: 7 height: 6
Every branch here has an area field.
I almost stole Rich Hickey's syntax, I hope he won't be offended ^_^
listLiteral =
| "{" spaces "}" -- emptyList
| "{" listElements "}" -- notEmptyList
listElements = primary whiteSpaces (","? spaces primary)*
hashSetLiteral =
| "#{" spaces "}" -- emptyHashSet
| "#{" listElements "}" -- notEmptyHashSet
As you can see from that grammar, commas between elements are optional.
list = {1 2 3 4}
list add: 5 //! {1 2 3 4 5}
list at: 0 //! 1
list copy at: 0 put: 5 //! {5 2 3 4 5}
list //! {1 2 3 4 5}
x = #{"sas" 1, "sus" 2}
x at: "ses" put: 3
set = #{1 2 3}
set add: 2 //! #{1 2 3}
set add: 4 //! #{1 2 3 4}
set has: 3 //! true
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.
P.S. u can find binary releases in the releases
After running compile.sh you will get niva_compiler folder that contains jvm or native binary.
sh compile.sh jvm
- run compiler from bin folder
- install graalvm
yay -S jdk21-graalvm-bin
and set it default:sudo archlinux-java set java-21-graalvm
on Arch,nix shell nixpkgs#graalvm-ce
on nix sh compile.sh bin
Niva can eat .niva and .scala files, because Scala highlight fits well for Niva :3
niva main.niva
- compile and run
niva main.niva -с
- compile only, will create binary for native target and fat-jar for jvm
niva main.niva -i > info.md
- will generate info about all code base of the projects, -iu
- only user defined files
niva run
- run all files in current folder with main.niva as entry point
niva build
- produce binary