"Easy C Lang"
eclang is a system language compiler which generates human-readable C source and aims for full interoperability with C.
- Full interoperability with C.
- Duck-typing, interfaces.
- Generating human-readable C source.
- Function overrides, Functional paradigms.
- Null safety.
- Pool memory management, defer, deep-clone, APR (Apache Portable Runtime).
- Modernized syntax (Inspired by languages such as Python, Kotlin, Go, Elixir).
- Error handling.
sudo apt update
sudo apt install gcc
sudo apt install maven
sudo apt install build-essential
sudo apt install libapr1 libapr1-dev
mvn clean package
java -jar ./target/eclang-0.1.0.jar -c ./examples/fib.ec -o out.c
gcc out.c -o out -I/usr/include/apr-1.0 -L/usr/lib -lapr-1
Include a .h or .c file with the use keyword.
use "stdio.h"
or ec files
use "lib.ec"
See: lib.ec
fn function_name(parameter_list): return_type do
// function body
or
fn function_name(parameter_list): return_type = // returning expression
Functions may also have overloaded definitions, default parameter values, and type constraints.
fn main(): Int do
log("Hello World")
ret 0
The generated C code:
int main_0(void) {
printf("Hello World");
return 0;
}
fn test(a: Int, b: Int & CString, c: Int = a): Int do
ret c
In this example:
aandbare mandatory parameters.cis an optional parameter that defaults to the value ofaif not provided.- Creates four overloads: (Int, Int), (Int, String), (Int, Int, Int), (Int, String, Int)
fn z(a: Int): Int = a + 1
This function returns the input integer a incremented by 1.
val char1: Char = 'a'
val char2: Char = 0
val char3: Char = 0c
val uchar1: UChar = 0
val uchar1: UChar = 0uc
val short1: Short = 0
val short2: Short = 0s
val ushort1: UShort = 0
val ushort2: UShort = 0us
val int1: Int = 0
val int2: Int = 0i
val uint1: UInt = 0
val uint2: UInt = 0u
val uint3: UInt = 0ui
val long1: Long = 0
val long2: Long = 0l
val ulong1: ULong = 0
val ulong2: ULong = 0ul
val float1: Float = 0
val float2: Float = 0.0
val float3: Float = 0f
val float4: Float = 0.0f
val double1: Double = 0
val double2: Double = 0.0
val double3: Double = 0d
val double4: Double = 0.0d
val string1 = "Hello World"
val string2: String = "Hello World"
val cstring1 = "Hello World"c
val cstring2: String = "Hello World"c
val array1 = ["One", "Two", "Three"]
val array2: Int[] = [5, 3, 6, 0]
val carray1 = ["One", "Two", "Three"]c
val carray2: Int[]c = [5, 3, 6, 0]c
not ! !! and && or || xor ^^
+ - * / % **
== != === !== < > <= >=
val a: CString = "Hello"c
val b: CString = "Hello"c
val c: Bool = a == b // true
val d: Bool = a === b // false
The above compiles into the following C code:
char* a = "Hello";
char* b = "Hello";
bool c = strcmp(a, b) == 0; // strcmp from string.h
bool d = a==b;<< >> <<< >>> & | ^
??
fn x(a: Int) do
log("Int")
fn x(a: String) do
log("Str")
Here, the function x is overloaded to handle both Int and CString types.
The first argument of a function can be used as a self, and called with the following syntax:
10.x()
val a: Int = 12
a.x()
if condition do
// code block
else if condition do
// code block
else
// code block
if true do
log("Test1")
else if false do
log("Test2")
else
log("Test3")
External functions allow you to interface with functions outside of the language's core syntax, such as C library functions.
declare fn "external_name" as alias_name(parameter_list): return_type
declare fn "printf" as log(value: CString): None
declare fn "printf" as log(format: CString, value: String): None
In the example above:
log(value: CString): Nonelogs a string without formatting.log(format: CString, value: CString): Nonelogs a formatted string.
Records are user-defined data structures that group related variables under a single name.
rec RecordName as
field1: Type1
field2: Type2
rec Test as
a: Int
b: Int
In this example, Test is a record with two fields: a and b, both of type Int.
Safe casting
val intValue: Int = 56
val longValue: Long = intValue as Long
Unsafe casting allows you to cast any type to any other type without restriction
val intValue: Int = 56
val pointer: @Int = intValue as unsafe @Int
Define custom named types:
declare type DefinedType = Int
Pointer types are defined with the at sign @. Addresses are given by the addrof keyword.
val variable: Int = 10
val addr: @Int = addrof variable
val otherVariable: Int = @addr
Generates the following C code:
int variable = 10;
int* addr = &variable;
int otherVariable = *addr;Nullable types allow a variable to have either a value of the specified type or be Null.
NullishCoalescing operator ?? can be used to transform a null type with a default value.
fn n(a: CString?): CString do
ret a ?? ""
The above example generates the following C code.
char* n_0(char* a) {
return (char*)__ec_nc(a, "");
}Or ignore the nullable type with a ! suffix.
fn n(a: CString?): CString do
ret a!
val k = :test_atom
fn a(atom: :log | :dont_log) do
if atom == :log do log(atom)
In this example:
kis inferred to be of typeAtom.- The function
aaccepts one of two possible atom type and logs the atom name if the:logatom is passed
rec Duck as
name: CString
coolness: Int
rec Goose as
name: CString
loudness: Int
fn quack(bird: Duck & Goose) do
log(bird.name)
fn quack(bird: <B>) do
log(bird)
quack("Hello")
quack(420)
inter ExampleInterface as
a: Int
b: Int
rec ExampleRecord impl ExampleInterface as
a: Int
b: Int
c: CString
Deferring can be used to clean up memory at the end of scope
fn defTest(input: DefinedType): Int do
defer log("1")
log("2")
if true do
defer log("3")
log("4")
else
defer log("5")
log("test")
ret 1
ret input
The above generates the following C code:
int defTest_0(int input) {
printf("2");
if (true) {
printf("4");
printf("3");
}
else {
printf(("test"));
int __ec_ret = 1;
printf("1");
printf("5");
return __ec_ret;
}
int __ec_ret = input;
printf("1");
return __ec_ret;
}Defer blocks of code:
fn defTest(input: DefinedType): Int do
defer do
log("1")
log("2")
log("3")
The global variable ROOT_POOL: @Pool? is intended to be the default pool to use when no pool is specified when invoking a function with takes a Pool as an optional.
ROOT_POOL is not initialized by default so your program needs to do it at the start of your main function or always provide a user defined Pool.
fn main(): Int do
createPool(addrof(ROOT_POOL!), NULL)
defer freePool(ROOT_POOL!)
log ("%s", "Cloned String".clone())
Where clone from lib.ec is implemented as such:
fn clone(str: CString, pool: @Pool?): CString do
val n = str.lengthOf() // string.h strlen
val newStr = alloc(pool ?? ROOT_POOL, sizeof(CString) * (n+1) as Size) as unsafe CString
copy(newStr, str, n) // string.h strncpy
ret newStr
fn clone(str: CString): CString = clone(str, NULL)