Express is an extremely flexible language allowing both static and dynamic types (var
; think JavaScript
or Python
— except better) with an extremely lite, dynamically embedded runtime within the executable allowing for typing to be as weak or strong (or 🦆) as required. By allowing even the typing to be defined within the language, many different programming paradigms, architectures, and methods of implementation can be readily expressed.
The runtime currently does dynamic typing and RAII lifetime management, however, in the future it will include a greenthread scheduler a la Go, atomization of cross-thread operations, garbage collection, RTTI and (maybe) reflection, SQLite3/GraphQL embedded databases, DOM instantiation and manipulation, and a few other ideas that are currently only conceptual. Most features will be optional with the ability to easily enable and disable them at compile time and possibly runtime (if a JIT/AOT is supported — maybe using GraalVM).
The main influences in the languages design are: C++
, JavaScript
, Go
, and (atleast conceptually) Rust
.
For binary production, programs are currently transpiled to C++ and then clang
is subsequently invoked (along with clang-format
) to produce the corresponding binary. There will also be a C++ program produced at compile time, which can be included in the output via the --emit-cpp
flag.
At this time, transpiling is, time-wise, sufficiently more efficient than outputting LLVM tokens or building an intermediary using SSA/3AC. Later on, this will most likely be changed in favor of direct LLVM token production when features either become too much of a burden to implement and maintain in C++ or the transpiler development lags too much to adequently support forwarding the development of the language.
Each stage of the compiler (lexer, syntax parser, semantics parser, and C++ transpiler) are currently all implemented in Go
and may be converted to C++
or Rust
later on (when I feel like operating LLVM directly), but a JavaScript
implementation in Node is also being developed simultaneously and will later on be consolidated with this repo after a reorganization of the file structure.
It is currently located at https://github.com/Swivelgames/Express/tree/alt/node
- Lexer
- Syntax
- Semantics
- C++ Transpiler with
clang-format
- Binary
[~] means the feature is still being decided on
[=] means I am currently working on implementing that feature
- Blocks
- Basic types (
int
,bool
,float
,string
) -
object
type- statements within objects (making objects and blocks identical structures)
-
struct
type- Tags
-
function
type - [-]
array
type-
int[]
-
float[]
-
bool[]
-
string[]
-
var[]
-
object[]
-
<struct>[]
-
array[]
-
function[]
- Type inference
-
- [-]
var
type- Basic type encapsulation (
int
,bool
,float
,string
) -
object
type encapsulation -
<struct>
type encapsulation- This is doing the same thing as casting your struct to an object
- [~]
array
type encapsulation- Leaning against: not sure if I want to allow a single
var
to be able to contain multiple values
- Leaning against: not sure if I want to allow a single
- [~]
function
type encapsulation- A var holding a function doesn't make a lot of sense right now
- Basic type encapsulation (
- Type modifiers - keywords
-
array
postfix -
unsigned
prefix -
constant
prefix
-
- Type modifiers - shorts
-
s
postfix -
u
prefix -
c
prefix
-
- [=] Function usage
- [=] Function declaration
- [=] no args and no returns
- [=] args without returns
- [=] returns without args
- args and returns
- Function call
- [=] Function declaration
- [-] Access modifiers
-
private
-
public
-
fileprivate
-
Proposal submission is the method to contribute your ideas and work into Express. The first step to submitting a proposal is a usually feature
request through a PR. Below contains the list of steps and the corresponding criteria for submission and approval.
Features:
Features should be submitted in the form of a markdown file (title_of_your_feature.md
) describing the proposed abilities into a folder withinproposals/features/<appropriately_labeled>
folder. Proposals need to use fenced code blocks with a combination ofcs
,vb
andjs
highlighting tags and contain an Express program example of your feature.
Ideas:
Ideas about the languages direction, internals related to the architecture, or general core-langaage level implementations requests should go into theproposals/ideas
folder and should be formatted as a whitepaper with detail containing a beginning narative that will explain where and how the idea came from, atleast two supporting arguments that should answer the question of why your proposal should be considered, and one informal/abstract (i.e, pseudocode) use-case on how it would be used after implementation.
Implementations and Experiments:
Implementations and Experiments should be done in a separate branch off of the repo after approval of a submittedfeature
request. When you are ready to submit your implementation, you should fetch the latest upstreammaster
and merge your code in locally, confirm that tests still work, and then submit a PR intomaster
.
Your PR should also contain an Express test program in thetest/output/programs
folder, along with the respectivelex
,syn
,sem
,cpp
, andbin
files that will be used to verify the tests.
Note
: If your PR does not meet the criteria when it is submitted, cannot be merged into the parent branch, or standard tests do not pass, it will be automatically denied by the CI/CD pipeline with comments about failure. You will need to submit another PR after fixing the mentioned issues.
Apologies before we get started, but this does need to be updated when I have time as there are quite a few features missing in contrast to what is supported. Please take a look at the programs located in test/programs/
for more examples.
In the program below, you will find a few examples of the allowed flexibilities and optional verbosity that allow the language to be so Expressive.
You can find the full uncommented version located in test/programs/advanced.expr
.
Let's Begin:
Start off by declaring some variables:
int powerlevel = 0x2327;
bool over9k = false;
float pi = 3.14159265359;
string arizona = "iced out boys";
Note
: If you're experienced in programming, you might have noticed that we did not begin by declaring amain
function or designated execution entry point; this is definitely not by design and will be enforced at a later date.
In addition to the basic static types, Express also supports using dynamically typed variables as well.
It is important to note that these variables are dynamically typed at run time and thus will incur a performance penalty in constrast to static variables correlating to the same shadow type.
// start 'hi_my_type_is' off as a dynamically typed string variable
var hi_my_type_is = "what"
// now i'm an integer
hi_my_type_is = 666
// here's a bool
hi_my_type_is = false
// and finally, a float
hi_my_type_is = 2.71828
For a closer look at how the runtime manages this in C++, see the source code in
lib/var.cpp
.
More documentation and better comments [ as if there is any, lol - really though :^) ] will be added later - i promise
Comments also take on a familiar syntax:
// Inline comment
/*
Multiline
comment
*/
Another way to declare variables in Express is through type inference. Both of the variables below are of type
int
, however the latter type was inferred based on evaluation of thervalue
:
int zero = 0
one := 1 // tabbed for visibility
Note
: Type inference will never produce avar
or astruct
type.
Fundamentally, thevar
type could be considered the ground state for any variable type and thus would always resolve as a possible type. Furthermore, if you are specifying to infer a variables type, it doesn't make much sense, functionally, to respond with a generic container.
On the other hand,struct
is a different issue. Structs and objects are very similar ideas, however, one is dynamic -object
, and the other is not -struct
. Thus, when assigning aBLOCK
to a variable in Express, it will assume you do not want this to be a staticstruct
type or else you would have declared it yourself at compile-time. However, this does not prevent you from declaring anobject
at compile-time or astruct
at run-time using a type specification at declaration.
Optional Verbosity
:
Before moving on, let's explain a primary motivator in the Express language development. Above, you might have observed that the usage of commas and semicolons as statement delimiters seems to be optional, and indeed they are! Statement delimiters are not required, but are acceptable (;
or,
) if you'd prefer to be more verbose or are accustomed to C-style programming.
In the underlying parser architecture, they (;
or,
) serve a semantic purpose by marking the end of a statement parse which will manually command the compiler to dump the current parse. By default, the ending of the statement will be semantically inferred, however, there are compilerflags
and ECMA-335attributes
to modify the default action and enforce strict punctuation as granularly (or entirely) as you prefer.
In this regard, having the flexibility to allow the compiler to semantically infer the end of the statement, while also retaining the ability to manually signal when a statement should end, can be very satisfying and relaxing when programming.
This allowed flexibility is known asoptional verbosity
in Express and is one of the key motivators in it's development.
Now, I wouldn't do this, but as a testament towards the semantic reasoning within the parser; you can even write statements on the same line as each other:
string ayy = "ayy" string waddup = "waddup" int timestamp = 1527799745
Usage of the
set
operator is permitted even outside ofobject
declarations.
anotherOne: "anotherOne"
Below shows a type inferred
object
where most of its properties are also type inferred. The following statements until the ending brace are all conatined within thetesterino
variable.
testerino := {
id: "ba3d4793-cfae-48d1-ad51-47cbfd70f98a"
Reference one of the above variables in a new property declaration
time: timestamp
You can also use the
assignment
operator along with a type to crimp the type of the variable instead of leaving it up for interpretation by the compiler
float price = 55.3592,
The
inference
operator can also be used within objects. Although currently it doesn't do anything different than theset
operator within an object, it may have a more impactful use later
dank_meme := true
Any unicode character is supported
🔥420🔥 : "🅱️laze it"
ⒹⒿKhalid : anotherOne
An
array
composition using the above definitions ofzero
andone
to derive atype
andlength
inference:
ten: [ one, zero, one, zero ]
A few nested objects
throw_more_dots: {
throw_more_dots: {
more_dots: {
more_dots: {
ok: "stop dots",
},
},
},
}
Ending of the
testerino
object
}
Arrays can be declared as well; below is a
static
string
typearray
using composition to infer an arraylength
:
string[] stringArray = [ "hi", "my", "name", "is", "scott" ]
Expanding on the above example, delineatiation of elements from one another using commas follows the same logic as the aforementioned statement delimiters. It isn't required but should be used at your own descretion of verbosity.
The spacing also doesn't matter, but readable code does.
Again - its all semantics¯\_(ツ)_/¯
string[] here_comes =
[
ayy,
waddup
"its",
"dat"
"boi"
]
Quick power level check before blasting off...
if powerlevel < 9001 {
powerlevel = 9001
over9k = true
}
A simple for loop:
percent: 0
for progress := 0, progress < 100, progress++ {
percent = progress
}
A key-based iterator for loop (for..in):
int i = 0;
for index in [ 1, 2, 4 ] {
i = index;
}
A value-based iterator for loop (for..of):
houstonWeHaveLiftOff := false
countdown := [ 9, 7, 6, 5, 4, 3, 2, 1 ];
for step of countdown {
// Get ready for take off
houstonWeHaveLiftOff = false
}
houstonWeHaveLiftOff = true
A simple function:
func lookAtme() {
// function contents...
something := "else"
}