Playground to create a new language named K# (Kotlin mixed and sharped with other languages) which should be compiled to .NET, JavaScript, and maybe others.
The language is hardly inspired by the Kotlin with bits of Swift, TypeScript, Scala, and C#.
While the baseline is the .NET itself, I wanted it to bring power to the JS world too. First, because I want to use the language full-power in the browser, and second, because I want to share the same codebase between the backend and the frontend, and, in the future, we have one for it, we can try to follow a path like Kotlin did with its multiplatform support.
This project is for FUN, LEARNING, and EXPERIMENTING with the knowledge area of Compilers in Computer Science (except that I will start skipping the creation of the parser by myself because I really want to work with the generated stuff. And, that's is the reason why I converted the project from F# to C# again. Maybe in the future I do that! Maybe... :D).
- Investigate a parser to play with (using Irony for now);
- Investigate how to parse and roughly generate the AST;
- Investigate how to create partial tests to verify the generated AST;
- Create a semantic analyzer to verify the generated AST;
- Translate code to JavaScript;
- Translate code to C#;
- Translate code to .NET IL;
- Translate code to WebAssembly (maybe);
- Check other possible backend targets;
K# is almost the Kotlin language with a few changes in the syntax and, obviously, a smaller set of features. I'm creating it for fun, I'm a father of a newborn baby (yes, I am not sleeping well by these days!), thus I don't have enough time to create something better for now...
val x = 10
val x: Int = 10
val name = "Daniel"
val finished = false
val finished: Bool = default
var x = 10
var x: Int = 10
var name = "Daniel"
var finished = false
var finished: Bool = default
K# Type | .NET Type | TypeScript Type |
---|---|---|
Char |
CharUtf16 |
CharUtf8 |
Utf8Char |
Byte |
Character |
Utf16Char |
Char |
Character |
Int8 |
SByte |
Number |
Int16 |
Int16 (short ) |
Number |
Int32 |
Int32 (int ) |
Number |
Int64 |
Int64 (long ) |
BigInt |
UInt8 |
Byte |
Number |
UInt16 |
UInt16 (ushort ) |
Number |
UInt32 |
UInt32 (uint ) |
Number |
UInt64 |
UInt64 (ulong ) |
BigInt |
Float32 |
Single (float ) |
Number |
Float64 |
Double (double ) |
Number |
Bool |
Bool |
Boolean |
String |
String |
String |
Utf8String |
Byte[] |
String |
Utf16String |
String |
String |
K# Type | .NET Type | TypeScript Type |
---|---|---|
Any |
Object |
any |
Unit |
Void |
void |
Number |
None |
Number |
Range |
Range |
[number, number] |
Index |
Index |
Index |
Named tuples
type PersonAge = (name: String, age: UInt8)
Unnamed tuples
type PersonAge = (String, Uint8)
By default, the types above don't accept null
. You have to explicitly define them
as nullable
to make that work.
For example:
val checked: Bool? = null
val enabled: Bool = null // it will show an error
Note: In C#, we have nullable reference types and nullable value types. In K#, we only have nullable types which are mapped to the first or second types before, when targeting .NET.
Note: In TypeScript, we nullable types can be supported as a union type with
null
orundefined
.
- Example: sum of two numbers:
val x = 10
val y = 20
val sum = x + y
- Example: sum of two numbers using a single variable:
var sum = 0
sum = sum + 10
sum += sum + 20
- Binary operators
<< N
shift left N bits>> N
shift right N bits<<< N
rotate left N bits ? (IDEA)>>> N
rotate right N bits ? (IDEA)
- Unary operators
-
negates a numbernot
negates a boolean value
- Binary operators
+
addition of two numbers-
subtraction of two numbers*
multiplication of two numbers/
division of two numbers%
remainder of the division between two numbers
!=
not equal==
equal<
less than<=
less or equal than>
greater than>=
greater or equal thanrefEquals
reference equality
not
logical NOTand
logical ANDor
logical ORxor
logical XOR
- if only
if value == true {
// do something
}
- if / else
if value == true {
// do something
} else {
// or do this
}
- if / else chain
if not goodBook {
// discard it
} else if hasMore {
// take another one!
}
- if / else if / else
if not goodBook {
// discard it
} else if gift {
// put it on the cabinet
} else {
// throw it away
}
when grade {
'A' -> print("Superb!")
'B' -> print("Very good!")
'C' or 'D' -> print("We need to study more...")
else -> print("Daddy is coming! Run!!")
}
while not stack.empty {
print(">> ${stack.pop()}")
}
for var i = 0; i < 10; i++ {
// old for style for
}
// by default, item type will be infered from the list item; also, it's read-only
foreach item in list {
// do something
}
// each value from the list will be casted to Int
foreach item as Int in list {
// do something
}
// in this case, the item is a mutable copy of the list item
foreach var item in list {
// do something
}
var i = 0
do {
// do something
} while i++ < 20
Enumerables:
foreach i in range(from = 0, to = 1, by = 1) {
}
// ternary-like
val grade = if points >= 9 then 'A' else 'B'
// chained ifs
val grade = if points >= 9 then 'A' else if points >= 8 then 'B' else 'C'
// multi-line chained ifs
val grade =
if points >= 9 then 'A'
else if points >= 8 then 'B'
else if points >= 7 then 'C'
else 'D'
val numberAsString = when digit {
0 -> "zero"
1 -> "one"
2 -> "two"
3 -> "three"
else -> "four"
}
// per-file namespace
namespace My.Helpers
// use non-ambiguous namespace inferred from the referenced libraries
using Kool
// explicitly specify the assembly or package and then import its namespace
using from "mscorlib" System.Numerics
All classes are reference types by default. However, there are value classes that are stored in the stack.
Additionally, there are other variants like data classes, which are types that will provide a default implementation
for equals
, hashCode
, and toString
methods, and some additional helpers methods for destructuring.
The syntax is:
[modifier = public] [partial] [value] [data] class $identifier [: $baseTypeOrInterface] {
...class_members*
}
class Person {
// mutable fields (use val for readonly)
var name: String = ""
// or
var name = "" // the type is inferred from the assignment
var surname: String? = null
var age: Int = 0
// or
var age = 0 // the type is inferred from the assignment
val fullName: String
get() {
return .surname is not null ? $"{.name} {.surname}" : .name
}
// or
val fullName get() = if .surname is not null then $"{.name} {.surname}" else .name
fun setName(name: String, surname: String, age: Int? = null) {
.name = name
.surname = surname
if age is not null {
.age = age // automatic casting from Int? to Int
}
}
override fun toString(): String {
return (
$"""
Person {
{nameof(.name)} = "{.name}"
}
"""
)
}
// or
override fun toString(): String =
$"""
Person {
{nameof(.name)} = "{.name}"
}
"""
}
Fields are defined using the var
or val
keywords. The var
keyword is used for mutable (non readonly) fields,
while the val
keyword is used for readonly fields. The type can be inferred from the assignment.
The syntax is: [modifier=public] (let|var) identifier [: Type] [= expression]
Note:
- The expression assignment can only be omitted for fields of a value class type, or a nullable reference type, in this
case the value assigned will be
default(Type)
.
Example:
class Point {
var x: Float32 = 0
var y = 0f // public field (default), type inferred from the assignment
private val scale = 1f // private field
}
class Math {
val pi = 3.14159265359
}
Properties are defined using an extension of the field declaration which includes a getter and/or a setter block.
The syntax is: (let|var) [modifier=public] [initonly] [required] identifier [: $type] ([= $value] | lateinit) $getter set $setter
Example:
class Example {
var x = 0f
var y = 0f
private var _steering = 0f
// full definition
let length: Float32
get() {
return Math.sqrt(.x * .x + .y * .y)
}
// simplified definition of code above, type omitted
let length get() -> Math.sqrt(.x * .x + .y * .y)
// property with backing field, type omitted and inferred from the assignment
var steering = 0f
get() {
return ._steering
}
set {
._steering = Math.clamp(value, -maxSteering, maxSteering)
}
companion {
const maxSteering = 20.0
}
}
An auto property is a property that has a default implementation for the getter and setter methods.
class Person {
var name = ""
get
set
}
val p = Person()
p.name = "Daniel"
writeLine(p.name) // prints "Daniel"
The field
keyword can be used to access the backing field of a property, which will be automatically generated for auto or semi auto properties.
class Person {
var name = ""
get() -> field
set(value) -> field = value
// which is equivalent to
var private _name@ = "" // backing field generated by the compiler not accessible from outside
var name: String
get() -> _name@
set(value) -> _name@ = value
}
The initonly
and required
keywords can be used in conjunction with the var
keyword to define properties that
can only be set during the object initialization.
// using field accessors
class Example {
var x = 0f
get() -> field
init(value) -> field = value
var y: Float32 = 0f
get() -> field
init(value) -> field = value
var z: Float32
get() -> field
required init(value) -> field = value
var w: Float32
get() -> field
required set(value) -> field = value
/**
* using auto-properties
*
* var x = 0f get, init
* var y: Float32 = 0f get, init
* var z: Float32 get, required init
* var w: Float32 get, required set
*/
}
let example = Example1(x = 10, y = 20, z = 30, w = 40)
example.x = 10 // error because x is initonly
example.y = 10 // error because y is initonly
example.z = 10 // Ok because the z is mutable
example.w = 10 // error because w is initonly required
let example2 = Example1() // error because z and w are required
class Calc {
// static methods can be defined in a static block
static {
fun sum(a: Int, b: Int): Int -> a + b
fun subtract(a: Int, b: Int) -> a - b
fun multiply(a: Int, b: Int) -> a * b
// the throws part is optional and merely a documentation that the method can throw an exception
fun divide(a: Int, b: Int) throws DivideByZeroException -> a / b
// or
fun divide(a: Int, b: Int) throws -> a / b
// or
fun divide(a: Int, b: Int) -> a / b
}
}
abstract class Greeter {
abstract fun greet(name: String): String
}
class EnglishGreeter : Greeter {
override fun greet(name: String): String -> "Hello, $name!"
}
class PortugueseGreeter : Greeter {
override fun greet(name: String): String -> "Olá, $name!"
}
import { Int, String, Runtime, assert } from '@k-sharp/runtime';
export class Calc {
static sum(a: Int, b: Int): Int {
return Runtime.add(a, b);
}
static subtract(a: Int, b: Int): Int {
return Runtime.subtract(a, b);
}
static multiply(a: Int, b: Int): Int {
return Runtime.multiply(a, b);
}
static divide(a: Int, b: Int): Int {
return Runtime.divide(a, b);
}
}
export abstract class Greeter {
abstract greet(name: string): string;
}
export class EnglishGreeter extends Greeter {
greet(name: string): string {
return `Hello, ${name}!`;
}
}
export class PortugueseGreeter extends Greeter {
greet(name: string): string {
return `Olá, ${name}!`;
}
}
All value classes are structs by default (in other words, they are stored into stack instead of heap).
value class Counter(var private value: Int = 0) {
fun increment() {
value++
}
operator implicit fun() = value
}
fun extend(counter: Counter) -> Counter {
counter.increment()
return counter
}
let source = Counter(10)
let result = extend(source)
print(source.value) // prints 0
print(result.value) // prints 1
print(if source === result then "equal" else "not equal") // prints "not equal"
NOTE: TypeScript doesn't support the native definition of value classes, then we emulate it by creating new instances on every mutation.
TypeScript:
import { Int, String, Runtime, Operator, assert } from '@k-sharp/runtime';
export class Counter {
constructor(private value: Int = 0) { }
increment(): void {
this.value++;
}
static [Operator.Implicit](self: Counter): Int {
return self.value;
}
}
export function extend(counter: Counter): Counter {
counter = Runtime.copy(Counter, counter);
counter.increment();
return counter;
}
const source = new Counter(10);
const result = extend(source);
writeLine(source.value); // prints 0
writeLine(result.value); // prints 1
writeLine(source === result ? "equal" : "not equal"); // prints "not equal"
// simple enum
enum ProcessStatus {
New,
Ready,
Run,
Terminated
}
var x = ProcessStatus.New
x = ProcessStatus.Ready
fun sum<T>(a: T, b: T) -> Int where T extends INumber<T> {
return a + b
}
import { type ReifiedCall, type TypeInfo, Runtime } from '@soil/runtime';
export function sum$T<T>($call: ReifiedCall<{ T: TypeInfo<T> }>): (a: T, b: T) => Int {
return Runtime.createGenericFunction($call, (a: T, b: T) => a + b);
}
K#
namespace Counting
class Counter(let count: Int) {
let double with get() = count * 2
fun increment() = Counter(count + 1)
fun decrement() = Counter(count - 1)
operator fun implicit() = count
}
let counter = Counter(10)
let value: Int = counter
C#
namespace Counting;
Counter counter = new(10);
int value = counter;
public class Counter(int count)
{
public int Count { get; } = count;
public int Double => Count * 2;
public Counter Increment() => new(Count + 1);
public Counter Decrement() => new(Count - 1);
public static operator implicit int(Counter counter) => counter.Count;
}
TypeScript
// imported
export type Int = number;
// probably it's better to not use a namespace here but another structure
export namespace Sample {
export const Ctr$Int = Symbol.for('new Counter(Int)');
export const OpImplicit$Counter$Int = Symbol.for('fun implicit (Int) -> Counter');
export class Counter {
constructor(public readonly count: Int) { }
get double(): Int {
return this.count * 2;
}
increment(): Counter {
return new Counter(this.count + 1);
}
decrement(): Counter {
return new Counter(this.count - 1);
}
static [OpImplicit$Counter$Int](counter: Counter): Int {
return counter.count;
}
static [Ctr$Int](count: Int): Counter {
return new Counter(count);
}
}
}
// translated code to keep semantic usage
const counter = Sample.Counter[Sample.Ctr$Int](10);
const value: Int = Sample.Counter[Sample.OpImplicit$Counter$Int](counter);
K#
Fibonacci.soul
namespace Sample
fun fibonacci(n: UInt) = if n <= 2 then n else fibonacci(n - 1) + fibonacci(n - 2)
C#
namespace Sample;
public static class FibonacciModule
{
public static uint Fibonacci(uint n) =>
n <= 2 ? n : Fibonacci(n - 1) + Fibonacci(n - 2);
}
TypeScript
export type UInt = number;
export namespace Sample {
export function fibonacci(n: UInt): UInt {
return n <= 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
}
K#
using Solid
using Solid.Html // provided by the Solid library binding
@file:Js(ignoreNamespace = true)
namespace Sample
class Counter : JsxNode {
let initialCount: Int? = null with get, init
fun render() -> JsxNode {
let (value, setValue) = createSignal(initialCount ?? 0)
let increment = { e' ->
setValue(value() + 1)
}
// translated to
return Div(
className = "counter",
children = [
Button(
type = "button",
onClick = increment,
children = [
Text(value())
]
)
]
)
}
}
class App : JsxNode {
fun render() -> JsxNode {
return Div(
className = "app",
children = [
Counter(initialCount = 10)
]
)
}
}
run<App>()
// require soil-solid-transformer
import { createSignal, render } from 'solid-js';
export type CounterProps = {
initialCount?: number | null;
}
export function Counter(props: CounterProps): JSX.Element {
const [count, setCounter] = createSignal(props.initialCount ?? 0);
const increment = () => setCounter(value() + 1);
return (
<div class="counter">
<button type="button" onClick={increment}>
{count()}
</button>
</div>
);
}
export function App(): JSX.Element {
return (
<div class="app">
<Counter initialCount={10} />
</div>
);
}
// the first call will return a cached function configured with the provided type parameters
run({ T: Runtime.getType(App) })();
using Solid;
using Solid.Html;
Run<App>();
public class Counter : JsxNode
{
public int? InitialCount { get; init; } = null;
public JsxNode Render()
{
var (count, setCounter) = createSignal(InitialCount ?? 0);
var increment = () => setCounter(count() + 1);
return new Div {
ClassName = "counter",
Children = [
new Button {
Type = "button",
OnClick = increment,
Children = [
new Text(count.Value))
]
}
]
};
}
}
public class App : JsxNode
{
public JsxNode Render()
{
return new Div {
ClassName = "app",
Children = [
new Counter {
InitialCount = 10
}
]
};
}
}
namespace Sample
value data class Point(val x: Float32, val y: Float32) {
val length: Float32 get() => MathF.sqrt(x * x + y * y)
func add(other: Point): Point => Point(x + other.x, y + other.y)
func subtract(other: Point): Point => Point(x - other.x, y - other.y)
static func operator +(left: Point, right: Point): Point => left.add(right)
static func operator -(left: Point, right: Point): Point => left.subtract(right)
static func operator ==(left: Point, right: Point): Bool => left.Equals(right)
static func operator !=(left: Point, right: Point): Bool => !left.Equals(right)
}
- Pass by value vs ref
func passByValue(value: Int) {
value += 1
}
func passByRef(ref value: Int) {
value += 1
}
var x = 10
passByValue(x)
print(x) // prints 10
passByRef(ref x)
print(x) // prints 11
- Closure
// shorter-version
func inc(increment: Int = 1) => (value: Int) => value + increment
// inner function inferred from outer type
func inc(increment: Int = 1): Func<(Int), Int> {
return value => value + increment
}
// all typed
func inc(increment: Int = 1): Func<(value: Int), Int> {
return (value: Int): Int => value + increment
}
func sum(array: Int[]): Int {
var sum = 0
for value in array {
sum += value
}
return sum
}
func sum(array: Int[]): Int {
return array.sum()
}
record class Point(
val x: Int = 0
val y: Int = 0
) {
constructor(other: Point)
: this(other.x, other.y)
prop length: Int => Math.sqrt(x * x + y * y)
}
// same as
class Point(x: Int = 0, y: Int = 0) {
private val _x: Int
private val _y: Int
init {
_x = x
_y = y
}
constructor(other: Point)
: this(other.x, other.y)
// full getter definition
prop x: Int {
get {
return _x
}
}
// simplified getter definition
prop y: Int {
get => _y
}
// fully-simplified getter-only definition
prop length: Int => Math.sqrt(x * x + y * y)
override func equals(other: Any): Bool {
return other is Point and x == other.x and y == other.y
}
override func getHashCode(): Int =>
HashCode.Combine(x, y)
open func deconstruct(): (Int, Int, Int) => (x, y, length)
}
// Possible React Integration through macro and protocols ???
protocol interface CounterProps for JsObject {
val initialCount: Int? = null
}
val Counter = component!{ (props: JsObject with CounterProps): JsxElement => {
val [count, setCount] = useState(props.initialCount ?? 0)
val decrement = useCallback(
{ setCount({ it - 1 }) },
[]
)
val increment = useCallback(
{ setCount({ it + 1 }) },
[]
)
return jsx!{
<div>
<button type="button" onClick={decrement}> - </button>
<span>{count}</span>
</button> type="button" onClick={increment}> + </button>
</div>
}
}
// maybe using types and macros
[macro:FuncComponent]
class Counter {
static operator func invoke(props: JsObject with CounterProps): JsxElement {
val [count, setCount] = useState(props.initialCount ?? 0)
val decrement = useCallback(
{ setCount({ it - 1 }) },
[]
)
val increment = useCallback(
{ setCount({ it + 1 }) },
[]
)
return jsx!{
<div>
<button type="button" onClick={decrement}> - </button>
<span>{count}</span>
</button> type="button" onClick={increment}> + </button>
</div>
}
}
}
// maybe using a bultin DSL
[FuncComponent]
object Counter implements IFuncComponent {
static operator func invoke(props: JsObject with CounterProps): JsxElement {
val [count, setCount] = useState(props.initialCount ?? 0)
val decrement = useCallback(
{ setCount({ it - 1 }) },
[]
)
val increment = useCallback(
{ setCount({ it + 1 }) },
[]
)
return jsx {
div {
button(type="button", onClick=decrement) {
text " - "
}
span {
text count
}
button(type="button", onClick=increment) {
text " + "
}
}
}
}
}
// transpiled to javascript ES2015
export class CounterPropsProtocol {
static get_initialCount(source, defaultValue = null) {
return source.initialCount ?? defaultValue
}
}
const Counter = (props) => {
const [count, setCount] = useState(CounterPropsProtocol.get_initialCount(props) ?? 0);
const decrement = useCallback(
() => { setCount(it => it - 1) },
[]
);
const increment = useCallback(
() => { setCount(it => it + 1) },
[]
);
return (
<div>
<button type="button" onClick={decrement}> - </button>
<span>{count}</span>
</button> type="button" onClick={increment}> + </button>
</div>
);
};
TODO Describe the language here