Skip to content

gavr123456789/Niva

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Niva

It's Smalltalk like language, but statically typed.

Name

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

Some strange demo

Peek_2024-02-22_02-09.mp4

Examples

You can start from some fibonacci here
And in this repo you can find WIP implementation of interpreter from the book https://interpreterbook.com/ Learn language here: https://gavr123456789.github.io/niva-site

Features

  • 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

Compile from sources

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.

Jar version

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/

Getting native binary with GraalVM

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

How to install GraalVM

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

Compile niva code

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.

Tutorial

Is here https://gavr123456789.github.io/niva-site

VSCode extension

  • LSP Server
  • VS Code extension full lsp support with autocompletion, error highlighting, goto definitions

Nix-Shell Setup

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>

Core

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).

Hello world

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

Type and methods declaration

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

Control frow

Messages

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 

Syntax

If

// 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"

Switch

// 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

If Elif Else chain

There is no elseIf syntax but you can chain simple then-else

x = 1

y = x > 2 => ">2" |=>
x < 2 => "<2" |=>
"???"
// y == "<2"

Program example: Factorial

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

Is even

Int isEven =
  this % 2 == 0 => true
  |=> false

5 isEven echo
4 isEven echo

Function call

There 3 types

5 factorial //unary 
5 + 5       // binary (only math symbols allowed)
map at: "key" put: "value" // keyword

Message cascading [Need design]

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;

Code blocks

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

Tagged unions

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.

Built-in collection literals

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

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}

Map

x = #{"sas" 1, "sus" 2}
x at: "ses" put: 3

Set

set = #{1 2 3}
set add: 2 //! #{1 2 3}
set add: 4 //! #{1 2 3 4}
set has: 3 //! true

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.

Compile from sources old

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.

JVM

  1. sh compile.sh jvm
  2. run compiler from bin folder

Native

  1. 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
  2. sh compile.sh bin

Usage

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