| title |
|---|
The E Programming Language |
The E Language is a simplified C language with fewer concepts and more
reasonable syntax. It is designed to be:
Explicit on pointer operations.Easy to learn and implement.- Designed for
Embedded systems andElectronic hobbyists.
Here are some comparisons of basic operations of C language and E language:
| C language | E language |
|---|---|
| &p | p@ |
| *p | p^ |
| ***p | p^^^ |
| p[3] | p[3] |
| &p[3] | p[3]@ |
| p + 3 | p[3]@ |
| (char *)p + 1 | p + 1 |
| p.m | p.m |
| (*p).m | p^.m |
| p->m | p^.m |
| char *p | p: byte^ |
| void **p | p: any^^ |
| ((struct Blah *) p)->f1 | p as (Blah^)^.f1 |
In C language,
p[2]is the same as*(p + 2 * sizeof(*p)), which is a waste. In E language,p + 2doesn't meanp + 2 * Nlike C language, it is justp + 2, whilep[2]is the same asp[2]in C language.
In E language:
arr: {word, 3} = {1, 2, 3};
struct Blah
id: word;
name: byte^;
end
b: Blah = Blah{id = 1, name = "hello"};
c: {Blah, 2} = {Blah{id = 1, name = "a"}, Blah{id = 2, name = "b"}};
In C language:
int arr[3] = {1, 2, 3};
struct Blah {
int id;
char *name;
};
struct Blah b = {1, "hello"};
struct Blah c[2] = {{1, "a"}, {2, "b"}};The most common usage of union is to reuse memory, which is convenient in
specific situations. But union can also be simply replaced by pointer
operations.
Here is an example about union in C language:
struct A {
char tag;
union {
ong long num;
har buf[8];
}
value;
};
struct A a = {.tag = 1, .value.num = 0x12345678 };
printf("%x\n", a.value.buf[2]);
//> 34In E language (and also in C language), we can use pointer manipulations to achieve this:
struct A
tag: byte;
value: word;
end
a: A = A{tag = 1, value = 0x12345678};
printf("%x\n", (a.value@ + 2) as (byte^)^);
%> 34
To keep things minimum, E language do not support union.
Enum is good, it brings better type checking. But on the other hand, everything
still works without enum.
To keep things minimum, E language do not support enum, either.
fn main(argc: word, argv: byte^^): word
return 0;
end
int main(int argc, char **argv) {
return 0;
}if fn1(fn2(val1)) >= fn3(val2) then
fn4();
elif val3 > 100 then
fn5();
else
fn6();
end
if (fn1(fn2(val1)) >= fn3(val2)) {
fn4();
} else if (val3) {
fn5();
} else {
fn6();
}while test() do
do_something();
end
while (test()) {
do_something();
}my_fn1: fn(): fn(): fn() = another_fn1;
my_fn2: fn(byte^; word): fn(byte^; byte^): fn(word; word): byte^ = another_fn2;
void (*(*(*my_fn1)())())() = another_fn1;
char *(*(*(*my_fn2)(char *, int))(char *, char *))(int, int) = another_fn2;For embedded systems, interrupt subroutines are important. To define an ISR:
fn exti_isr() attribute(interrupt(26))
%% Clear interrupt flag.
exti4^.INTF = 0b10000;
%...
end
The 26 indicates the interrupt ID which can be found in the chip
documentation.
When writting C code, users usually need to read/write assembly files and linkers files to make ISR work. We don't need to do those things in E language, we can write ISRs as long as we have the chip document. This is one of the features that make E language friendly to electronic hobbyists.
In C language, there is a type called void which is used for 2 purpose:
- For function definition,
voidindicates the function do not have parameters or return value. - For pointers,
void*stands for a pointer who can point to any type.
The logic of C language is: When we dereferncing
void*, we will got avoidtype who can not be part of an expression, so some usage bugs can be found by this design. This is not the best design since many C programmers do not understand this logic, they just remembered the rule.
In E language, there is no void type exposed to users.
For pointers, we use any instead. any^ in E language is same as void* in C
language.
For function definitions, we do not use void for parameter or return
type. When we define functions without parameters or return value, we just left
them out.
In C language, we can not just left out parameters for historical reasons: no parameter and void have different meanings. We always need a
voidto make our code robust.
In C language, any expression can act as a boolean expression, which have resulted in many many wrong code.
People always write wrong code like this:
if (a = b) {
//...
}In E language, there are only 6 boolean expression:
>, >=, <, <=, !=, ==.
So the following code will be refused by the compiler:
if a = b then
%...
end
Error message:
./sample/led_sample_1.e:115:9: invalid boolean expression for if
Since we use ^ as pointer operator (^ looks like a pointer), we can not
follow the C style. (C use ^ as xor)
Instead, E language choose the Erlang style for bit operations.
E language defined keywords band, bor, bnot, bxor for bitwise logic
operations, which is just like &, |, ==, ^` in C language.
And bsl, bsr for shift operations, which is just like \<\<, \>\> in C
language.
Bitwise and shift operations are important, but they are not as common as
pointer operations, this is the main reason E language choose ^ as pointer
operator.
A macro preprocessor is supported. Defining macro in E language is almost the same as in C language.
#define GPIOD (0x4001_1400 as (GPIO^))
But parameters are not supported.
Referencing a macro is different from C language (but same as Erlang). We need a
? mark before the macro name.
?GPIOD^.BSH = 0b1_1101;
In C language, we can't tell whether a symbol is a macro or variable or function, which make code hard to understand. By introducing the
?mark, we can always know it's a macro.
The compiler compiles E language source file to RISC-V (32bit RV32I/RV32E) machine code directly.
To call the compiler, we can call the command line tool ec.
Example for CH32V307:
ec -i ./sample/ch32v.e ./sample/led_sample_1.e -o /tmp/a \
--v-pos 0 --v-size 416 \
--c-pos 416 \
--d-pos 0x2000_0000 --d-size 64K \
--v-init-jumpExample for CH32V003:
ec -i ./sample/ch32v.e ./sample/led_sample_2.e -o /tmp/a \
--v-pos 0 --v-size 156 \
--c-pos 156 \
--d-pos 0x2000_0000 --d-size 2K \
--v-init-jump --prefer-shiftWe will get 2 bin files: a.code.bin (our code) and a.ivec.bin (interrupt
vector table). Then write them into the right address.
If you are an Erlang user, you can also call the compiler from erlang shell:
e_compiler:compile_to_machine1(["./sample/ch32v.e", "./sample/led_sample_1.e"],
"/tmp/a", #{...}).To build the compiler, read BUILD.md.
mkdir -p ~/.emacs.d/misc/
cp ./misc/emacs/elang-mode.el ~/.emacs.d/misc/Then add the following configurations to $HOME/.emacs:
(add-to-list 'load-path "~/.emacs.d/misc/")
(require 'elang-mode)
Vim:
mkdir -p ~/.vim/pack/wallace/start/
cp -r ./misc/vim ~/.vim/pack/wallace/start/elangNeoVim:
mkdir -p ~/.local/share/nvim/site/pack/wallace/start
cp -r ./misc/vim ~/.local/share/nvim/site/pack/wallace/start/elangThen add the following configurations to $HOME/.vimrc:
autocmd BufRead,BufNewFile *.e setlocal filetype=elang