Jina
1, Jina
Jina is a programming language with sane memory management, actors, and a coherent type system and syntax
Mahsa Jina Amini was a 22 years old girl, murdered by the evil Islamic regime in Iran
at her funeral, these words were written on a stone above her grave:
beloved Jina, you will not die, your name will become a code
Jina has sane memory management, ie there is no garbage collector
to achieve that, Jina introduces three special markers for types:
, unique type: T
, immutable shared type: T$
, immutable borrow type: T&
, mutable borrow type: T!
when a variable with unique type is aliased by another variable of the same unique type:
, if the original variable is not used in the following block (directly or through its borrowers),
it's a shallow copy (heap part will not be copied)
, otherwise, it's a deep copy
variables with borrow types will be tagged by their owner
borrow tags will be inherited by further borrowers
function arguments with borrow type are tagged with integers, starting from zero
for functions with multiple borrow arguments, that also return a borrow type,
and the scope tag of returned type is inherited from any borrow argument other than the first one (index 0),
in the return type, it must be indicated that which borrow argument will the tag be inherited from
A, B&, C& -> C&2
the borrow tag of variables captured in a closure, will be prefixed with "PARENT_"
only borrows with matched tags can be assigned to each other (checked at compile'time)
this simple rule automatically guarantees correct lifetimes, cause it means:
, we can't assign a locally created (or moved) value, to a mutable borrow variable captured in a closure
, we can't return a borrow from a function, unless it's originally borrowed from one of the function's arguments
mutation of unique types, can only be seen by one variable
mutable borrow types allow shared mutation
dealing with allocated memory, at the end of functions:
, for unique types, if they are not moved out of the function, their heap memory will be deallocated
, for shared types, the reference count will be decreased by one, and when it reaches zero,
their heap memory will be deallocated
, for borrow types, since they don't own the value, nothing happens
aliasing rules in variable definitions (and also assignments, in case of mutable ones, ie T and T!)
when a variable of type "T" is put into:
, a variable of type "T", shallow/deep copy
, a variable of type "T$", shallow/deep copy
, a variable of type "T&", shallow copy
, a variable of type "T!", copy address
when a variable of type "T$" is put into:
, a variable of type "T", deep copy
, a variable of type "T$", shallow copy, increase reference count
, a variable of type "T&", shallow copy
, a variable of type "T!", it's compile time error
when a variable of type "T&" is put into:
, a variable of type "T", deep copy
, a variable of type "T$", deep copy
, a variable of type "T&", shallow copy
, a variable of type "T!", it's compile time error
when a variable of type "T!" is put into:
, a variable of type "T", deep copy
, a variable of type "T$", deep copy
, a variable of type "T&", shallow copy
, a variable of type "T!", copy address
in Jina, type definitions containing borrow types, must have a borrow marker themselves:
T& := A, B$, C&, D!
this is necessary for their scope tags to be statically determined
in other words, borrows can't be wrapped, they always leak out
so plain types (without borrow markers) can only contain unique and shared types:
T := A, B$
of course, it can be aliased by variables with types that have markers:
v :T$
v.0 :A$
v.1 :B$
v :T&
v.0 :A&
v.1 :B&
v :T!
v.0 :A!
v.1 :B&
in Jina, functions are closures, ie they can capture their environment
each function has its own distinct type that includes the type of the captured variables
but we usually do not work with function types directly
what we care about is the interface that a particular function implements
f[t::(A,B->C)] :t& = { a :A, b :B -> C | ... }
f[t::(A,B->C)] :t = { a :A, b :B -> C || ... }
a borrow closure (with type "t&") can have borrowed captured variables; an own closure (with type "t") can't
sharing mutable data in concurrent parts of a program is problematic
a data race happens when these three behaviors occur:
, two or more pointers access the same data at the same time
, at least one of the pointers is being used to write to the data
, there's no mechanism being used to synchronize access to the data
to deal with it, programming languages choose different approaches:
, some implement complicated and error prone lock mechanisms
, some abandon concurrency, and make single threaded programs
, functional programming languages avoid mutability as much as possible
and when mutability is necessary, they use monads or algebraic effects to control shared mutability
avoiding mutability, and the need for aggressive garbage collection, is not good for performance
Jina uses actors for asynchronous programming, and controls aliasing (sharing) and mutability using type markers,
ie the same approach used to deal with memory management
messages sent to actors are own closures, and can't capture borrows
their captured variables have either unique or shared (immutable) type
thus messages can safely be called concurrently, since they can't change their captured environment
actors are opaque types, ie their internal components are not accessible,
and are referred to by an ID, instead of a memory address
actors (and their messages) can only be destroyed explicitly
reference counting combined with interior mutability can create reference cycles
in Jina interior mutability is only possible using actors
since actors are not reference counted, there is no reference cycles in Jina
types show us what we can do with the data, ie which operations are valid
subtyping is problematic:
i feel that this problem was the motivation behind dynamic typing (another bad design)
to avoid this problem, some languages (eg Rust) have two kinds of types:
, concrete types can be instantiated, but cannot have subtypes
, abstract types (also called traits or interfaces) cannot be instantiated, but can have subtypes
Rust has trait objects (dynamic interfaces) with the same problem regarding variance
https://users.rust-lang.org/t/vector-covariant-in-its-element-type/80582
https://stackoverflow.com/questions/55200843/what-does-it-mean-that-box-is-covariant-if-boxdyn-b-is-not-a-subtype-of-boxdy
in Jina we have interfaces, but there is no dynamic interfaces
instead, in Jina we have convertible types
it's like in mathematics where we can easily do arithmetic across integer, rational, real and complex numbers
when type "A" is convertible to type "B", it implements a special method named "into'B"
then when an expression of type "A" is used in a place that wants type "B",
the compiler automatically inserts ".to'B"
the idea is to only do this for types that can be converted with negligible run'time overhead
Jina compiles to C++, and thus can easily use existing C/C++ libraries
C++ (unlike Rust) allows us to have packages that are compiled independently as dynamic libraries,
then imported and used in other packages
Jina is installed by default in SPM Linux
in other Unix'like systems:
, first install "spm", by following the instructions mentioned in SPM Linux project
, then run this:
spm install $gnunet_namespace jina
2, syntax
comments:
;; comment line
;; comment
block
;;
comment
block
;
identifiers (variable names) start with an alphabetic character, and can include numbers and apostrophe:
abc'efg0123
defining a variable with unique type:
v :T = ...
v = ...
defining a variable with mutable borrow type:
v :T! = ...
v! = ...
defining a variable with immutable borrow type:
v :T& = ...
v& = ...
defining a variable with shared type:
v :T$ = ...
v$ = ...
assignment to mutable variables (ie those with unique or mutable'borrow type):
v. = ...
numeric types:
, Float: floating'point numbers with arbitrary precision and ball arithmetic
, Float2: floating'point numbers with double precision ("double" in C++)
, Float1: floating'point numbers with single precision ("float" in C++)
, Int: arbitrary sized integer
, Int8: 8 bytes integer ("std::int64_t" in C++)
, Int4: 4 bytes integer ("std::int32_t" in C++)
, Int2: 2 bytes integer ("std::int16_t" in C++)
, Int1: 1 byte integer ("std::int8_t" in C++)
, Int'u: word'sized unsigned integer ("std::size_t" in C++)
, Int'u4: 4 bytes unsigned integer ("std::uint32_t" in C++)
, Int'u2: 2 bytes unsigned integer ("std::uint16_t" in C++)
, Int'u1: 1 byte unsigned integer ("std::uint8_t" in C++)
, Int'c: equivalent to "char" in C++
numeric literals:
1.0 ;; Float2
1.23e4
0x1:0
0x1:23p4
1'234'567'890 ;; Int4
0x1234'5678'9ABC'DEF0
'a' ;; Int'c, a byte whose value is the ASCII code for character "a"
'\n' ;; a byte whose value is the ASCII code for the character with escape code "\n"
'\x12' ;; a byte with hexadecimal value 0x12
numbers are automatically converted when there is no data loss:
i :Int8 = 123
i = 123.to'Int8
complex numbers can be made of any type implementing the "Num" trait
c :Complex[Int4] = 1 + 1i
c = 1 + 0i
c = Complex[Int4] 1 0
lists:
l :List[Int'c] = ['a', 'b', 'c']
indexing:
e :Maybe[Int'c] = l_1
note that the type of the result of indexing above is actually "Maybe[Int'c]&",
which is copied into the variable with type "Maybe[Int'c]"
mutating lists:
l.put 'd' at: 3 ;; ['a', 'b', 'c', 'd']
l.put 'e' at: 0 ;; ['e', 'b', 'c', 'd']
character strings are UTF8 encoded, and implemented as a list of "Int'c" values:
s :Str = "abc def"
string interpolation: "abc{x}"
alternative syntax that makes writing single word strings easier:
'abc
multiline string equivalent to "first line\n\tsecond line":
s = "
first line
second line
"
it's better to split long strings into a list of single'line strings:
s = """
first line
second line
"""
which is equivalent to:
["first line", "\tsecond line"]
dictionaries are indexed using strings (instead of "Int'u" as in lists):
d :Dict[Int4] = [a: 1, b: 2, c: 3]
indexing:
d_'a
records:
r :(Int4, Int4, c: Int4) = 1, 2, c: 3
to access elements:
r.a
r.0
multiple assignment using record expansion:
a, b = 1, 2, 3
tagged fields can be expanded too, if the name of variables match:
a, b = a: 1, b: 2
mutating a record field:
r.a = 10
small records will be kept on stack, big ones on the heap
function:
f = { a :A&, b :B! -> C | ... }
note that the type of above function is:
(a: A&, b: B! -> C)&
function call:
f x y
which is equivalent to these forms:
f(x, y)
f b: y a: x
x, y >> f
default values for parameters:
f = { a = 1 , b = 2 | ... }
f b: 22
f 11
f()
conditional expression:
condition .then {} .else {}
condition1 .then {} .elif {condition2} {} .elif {condition3} {} .else {}
and or:
a && b
a \\ b
which are equivalent to:
a .and {b}
a .or {b}
not: -a
there is no loop construct directly available in Jina
instead we have iterators with "each" method, that is internally implemented using C++ loop
iter.each { x | ... }
any type that implements "Iter" trait, must define an "iter" method,
whose returned value stores the mutable state
actor:
a :Actor[S] = Actor.new S.new()
a.do { b :S! || ... }
modules are files containing definitions
any definition whose name is the module's name, will be exported (ie are accessible outside of the module)
exported names can have these kind of extensions:
, a number
, a postfix starting with an apostrophe
, letters inside brackets
to access the definitions in a module which is inside a directory: dir.Definition
to hide a module so it can't be accessed from modules in the parent directory, or outside of the package,
append an apostrophe at the end of its file name
a package is a collection of modules
in a project directory, we can have multiple package directories
the name of the package directory is the package name, plus a ".jin" extension
packages are of two kinds:
, application packages that contain a file named "0.jina" (which must contain the init function)
, library packages
to use a library package in a another package, create a file named "your'chosen'name.p" with this format:
, first line is the name of the package
, second line is the URL of the project containing the package
, the format of the URL (where protocol can be gnunet, git):
protocol://address'of'the'project'containing'package'directory
, the third line can contain a public key, which will be used to check the signature provided by the project
if there is no URL, it refers to the current project
to use the definitions of a package:
dot'p'file'name.Definition
definitions in "std" package, are directly accessible
so there is no need for a "std.p" file, and prefixing with "std."
type definition:
T := a: A, b: B
defining methods (using a namespace):
;ns T
new = { a :A -> T |
b = ...
a, b
}
m1 = { self!, x :X -> Y |
...
}
m2 = { self |
...
}
to create an instance of the type:
i :T = a: x, b: y
i = T a: x, b: y
i = T.new a: x
accessing a member:
i.a
calling a method (note that there is no record field named "m", otherwise this will not work):
i.m x
which is equivalent to:
T.m i x
if a method has no argument (other than self), it can be called like this: i.m
enums:
Bool := #true #false
x :Bool = #true
x = Bool#true
type "?A" is a shortcut for "Maybe[A]"
Maybe[t] := #result t #null
x :?A = #null
x >> {
#result x | ...
#null | ...
}
intefaces:
;i I
m1 ::(self!, x: X -> Y)&
m2 = { self! |
;; default implementation
}
interface inheritance:
;i I ::I1 ::I2
...
defining the methods of a type that implements some interfaces:
;ns T ::I1 ::I2
m1 = ...
m2 = ...
;; I1
m3 = ...
m4 = ...
;; I2
...
generics:
T[g] := a: g, b: g
bounded generics:
T[x::I] := a: x, b: x
to directly enter C++ code:
;cpp ...
...
or:
;cpp
...
;
beware! with great power comes great responsibility