The premise
Tachyon is a just-in-time compiled programming language optimized for mathematical operations. It serves as a personal project, and includes a virtual machine, bytecode, high-level intermediate representation, and abstract syntax tree. It employs various optimization techniques such as pure function memoization and just-in-time compilation to create fast and efficient programs during runtime.
Motivation
I wrote my first programming language following Crafting Interpreters part III. when I was 14 years old. That was my first introduction to the world of compilers and interpreters and I have been hooked ever since. Since then, I have written multiple programming languages for various tasks, but until now, I have never combined the two concepts to write a just-in-time compiler. In order to get a better understanding of how famous languages like JavaScript and Java work under the hood, I decided to take on this project and develop my own just-in-time language.
Architecture
The overall architecture of Tachyon is split into two key pipelines:
- Compile-time Pipeline
The compile-time pipeline consists of:
- Lexer: turns the source text into a list of tokens
- Parser: turns the list of tokens into an abstract syntax tree
- Bytecode Generator: turns the abstract syntax tree into a list of bytecode instructions
By the end of this pipeline, Tachyon has a list of bytecode instructions that represent the code written by the user.
- Runtime Pipeline
The runtime pipeline consists of:
- Tachyon Virtual Machine: interprets the bytecode, executes machine code, tracks/marks functions for optimization
- Intermediate Representation Generator: generates intermediate representation from bytecode
- Intermediate Representation Optimizer (incomplete): applies various optimizations to intermediate representations
- Machine Code Generator: generates machine code from the intermediate representation
Why take the step of generating intermediate representation, when one could directly attempt to optimize the bytecode (or ast)? The reason is that optimizing these structures is extremely difficult and, even if implemented correctly, compute-intensive. It is easier to generate a representation of the intent of the code (i.e. abstract individual instructions into an intermediate representation) than to optimize the instructions directly. Once the machine code is ready, it is passed to the Tachyon Virtual Machine which executes it. Furthermore, we have pure function memoization: during bytecode generation, the kinds of instructions are recorded. If all instructions are pure, the function overall is marked as pure, and the virtual machine is able to cache its input and return values during runtime. Function impurities propagates to parents. If function a calls function b that is impure, a is implicitly impure.

Figure 1: compile-time and runtime pipelines
Optimizations
As an aside, I decided to go through and optimize my lexer. My original lexer implementation's performance looked like this:
/home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
2025-07-14T09:45:45-04:00
Running /home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
Run on (8 X 3100.1 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x4)
L1 Instruction 32 KiB (x4)
L2 Unified 256 KiB (x4)
L3 Unified 6144 KiB (x1)
Load Average: 3.12, 2.31, 1.89
-------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------
LexerDataFixture/LexBlank 63.8 ns 63.1 ns 11318084
LexerDataFixture/LexBasic 1381 ns 1369 ns 457484
LexerDataFixture/Lex1000Chars 18119 ns 17961 ns 39048
LexerDataFixture/Lex2000Chars 34216 ns 33844 ns 20672
LexerDataFixture/Lex5000Chars 151496 ns 149748 ns 4715
LexerDataFixture/Lex10000Chars 390965 ns 386551 ns 1768
LexerDataFixture/Lex20000Chars 786364 ns 778392 ns 865
LexerDataFixture/LexBigComment 21277 ns 21048 ns 33004
Process finished with exit code 0
Which, by itself, is not bad. It takes about 1ms to lex 20000 characters, which is already a
significantly large Tachyon program. The benchmark program 06-20000-chars.tachyon is over 1000
lines of code. However, there are some key performance issues that I think can provide
significant performance benefits to Tachyon. By the end of this performance refactor, my
benchmarks will look like this (target RelWithDebInfo):
/home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
2025-07-14T12:09:32-04:00
Running /home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
Run on (8 X 3100.2 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x4)
L1 Instruction 32 KiB (x4)
L2 Unified 256 KiB (x4)
L3 Unified 6144 KiB (x1)
Load Average: 1.72, 1.55, 1.49
-------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------
LexerDataFixture/LexBlank 113 ns 112 ns 6341061
LexerDataFixture/LexBasic 287 ns 286 ns 2402244
LexerDataFixture/Lex1000Chars 3662 ns 3650 ns 172662
LexerDataFixture/Lex2000Chars 6921 ns 6899 ns 102302
LexerDataFixture/Lex5000Chars 30090 ns 29996 ns 23380
LexerDataFixture/Lex10000Chars 71156 ns 70868 ns 9913
LexerDataFixture/Lex20000Chars 134971 ns 134296 ns 5357
LexerDataFixture/LexBigComment 17539 ns 17469 ns 39073
Process finished with exit code 0
Notably, lexing 20000 characters received a 619% performance boost, and now runs faster than it used to take to lex just 5000 characters. You may notice that the blank fixture is now running ~50ns slower. I think this is because of when I reserve lots of memory for the token and constant vectors. Since this is only a 50ns loss for something that saves lots more with larger inputs, I'm o.k. with this loss. You can see, for example, that LexBasic is already much faster.
Below, you can find a quick walkthrough behind the thought process for each optimization. You
can find the pre-optimization code at or before commit
48f2f4a260c7e877209d46f064bea549b335d03b,
and the post-optimization code at or after commit
1770d4c62dcce461e8c9ede78bfea75183c03e06.
- Keyword hash tables
One such key performance issue was the keyword lexing process. In the old implementation, keyword recognition consisted of:
// ...
else if (s.substr(pos, 4) == "True" && !isalpha(s[pos + 4])) { /* ... */ }
else if (s.substr(pos, 5) == "False" && !isalpha(s[pos + 5])) { /* ... */ }
else if (s.substr(pos, 6) == "import" && !isalpha(s[pos + 6])) { /* ... */ }
else if (s.substr(pos, 2) == "fn" && !isalpha(s[pos + 2])) { /* ... */ }
else if (s.substr(pos, 6) == "return" && !isalpha(s[pos + 6])) { /* ... */ }
else if (s.substr(pos, 5) == "while" && !isalpha(s[pos + 5])) { /* ... */ }
else if (s.substr(pos, 5) == "break" && !isalpha(s[pos + 5])) { /* ... */ }
else if (s.substr(pos, 8) == "continue" && !isalpha(s[pos + 8])) { /* ... */ }
Which has way too many instructions per keyword occurence:
- take a substring of a certain length
- compare it to the keyword string
- ensure that the next character is not an alphanumeric value
- repeat for each kind of keyword (import, fn, return, ...)
I need to ensure that the next character is not an alphanumeric value so that identifiers
such as returned or continued are possible. As I discover in this implementation, it
was much easier to just parse the whole thing as an identifier first, then attempt match
it to a keyword based on length.
I was inspired by a YouTube video by Strager to use
a perfect hash table to increase performance. For this, I used a GNU tool called
gperf. Gperf generates a perfect hash function given a set
of keywords (which you can find in /libs/lexer/keywords.gperf). If the input string matches the
generated hash, it returns a Keyword type that has the corresponding TokenType that we are
looking for.
This reduces the instructions per keyword occurence to:
- hash the keyword
- compare the input to the hashed pair
Which is significantly faster. And since this in a "hot path" of the code (the loop where the lexer does work the most often), performance gains add up very quickly, causing a large boost in performance.
In Strager's video, he continues to beat gperf through various other techniques. However, since I only need a static map, I'm happy with the system that I have now. It may be worth looking into other techniques that he uses later.
One huge advantage of gperf (as opposed to manually writing a perfect hash function, for example) is that it's a generator tool, so adding new keywords to the lexer just consists of describing them in the gperf input (keywords.gperf) and regenerating the source. Hand writing and testing a perfect hash function like in Strager's video is doable for a language like JavaScript, where the standard-defined keyword set is unlikely to change tomorrow. Tachyon, however, is still very much a work in progress, so it helps a lot that modifying keywords is a fast part of my development workflow.
- Reserving capacity for token and constant lists
Another thing that I thought of was that we must be spending so much time on vector reallocations
for token and constant lists. When a vector is initialized with std::vector<T> v;, it starts
with a capacity of 0. It then scales geometrically when values are inserted, usually growing by
1.5-2x. As an example, let's assume a growth factor of 2x (implementation defined), and chart the
sizes of the vector as the lexer continues lexing tokens.
| Step | Capacity |
|---|---|
| Initial | 0 |
| Push first token... | 4 |
| Push fifth token... | 8 |
| Push ninth token... | 16 |
| Push 17th token... | 32 |
| Push 33rd token... | 64 |
| Push 65th token... | 128 |
| Push 129th token... | 256 |
| Push 257th token... | 512 |
| Push 513th token... | 1024 |
Each row is a reallocation. Reallocations are expensive because they consist of:
- a
malloccall to allocate new heap data at the larger capacity - a
memcpycall or a move/copy constructor to move existing data to the new address
Therefore, a significant performance boost can be found by reserving some size during the vector's initialization, avoiding reallocations (and if there are reallocations, they will be less frequent, since the capacity is already large and the vector scales geometrically). This comes with the cost of a larger memory footprint, especially for smaller source codes that will never reach the pre-allocated limit. Since Tachyon aims to be performant over memory efficient, I'll accept this cost. If I was working with embedded or other memory-limited devices, this decision may not have been so straightforward.
From some manual benchmarking, I found a good capacity of around 20000 tokens and 10000 constants.
Now, my performance looks like this, which is over a 3.4x improvement for Lex20000Chars just with
these last two optimizations:
/home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
2025-07-14T09:53:22-04:00
Running /home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
Run on (8 X 400.007 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x4)
L1 Instruction 32 KiB (x4)
L2 Unified 256 KiB (x4)
L3 Unified 6144 KiB (x1)
Load Average: 2.51, 2.42, 2.13
-------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------
LexerDataFixture/LexBlank 1303 ns 1251 ns 459511
LexerDataFixture/LexBasic 6082 ns 5859 ns 100000
LexerDataFixture/Lex1000Chars 6900 ns 6858 ns 101324
LexerDataFixture/Lex2000Chars 12601 ns 12530 ns 52799
LexerDataFixture/Lex5000Chars 53187 ns 52913 ns 13641
LexerDataFixture/Lex10000Chars 126448 ns 125637 ns 5658
LexerDataFixture/Lex20000Chars 226623 ns 225215 ns 2963
LexerDataFixture/LexBigComment 14150 ns 14020 ns 50649
Process finished with exit code 0
- Replacing
std::stod
These two optimizations are the only things that came to mind off the bat. From now on, I'll use
gbench and perf to get a better understanding of the lexer and where I can gain performance.
perf record -g ./lexer_benchmarks --benchmark_filter=LexerDataFixture/Lex20000Chars
perf report
Samples: 3K of event 'cycles:Pu', Event count (approx.): 3299930785
Children Self Command Shared Object Symbol
+ 55.46% 54.33% lexer_benchmark lexer_benchmarks [.] tachyon::lexer::Lexer::lex(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)
+ 12.83% 12.83% lexer_benchmark libc.so.6 [.] __GI_____strtod_l_internal
+ 6.51% 0.00% lexer_benchmark [unknown] [.] 0x005555555fa66000
+ 5.32% 3.12% lexer_benchmark lexer_benchmarks [.] LexerDataFixture_Lex20000Chars_Benchmark::BenchmarkCase(benchmark::State&)
+ 4.53% 4.18% lexer_benchmark libc.so.6 [.] round_and_return
+ 4.30% 3.15% lexer_benchmark libc++.so.1.0 [.] std::__1::stod(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, unsigned long*)
+ 3.35% 3.35% lexer_benchmark libc.so.6 [.] __strlen_avx2
+ 3.16% 2.58% lexer_benchmark libc.so.6 [.] __memmove_avx_unaligned_erms
+ 3.13% 3.11% lexer_benchmark libc.so.6 [.] str_to_mpn.part.0.constprop.0
+ 3.08% 0.00% lexer_benchmark [unknown] [.] 0xaa8000736e6f0073
+ 2.89% 1.85% lexer_benchmark libc.so.6 [.] __ctype_b_loc
+ 2.81% 2.16% lexer_benchmark lexer_benchmarks [.] decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<2ul>::__dispatch[abi:ne190107]<std::__1::__variant_detail::__dtor<std::__1::__variant_detail::
+ 2.56% 1.04% lexer_benchmark lexer_benchmarks [.] __ctype_b_loc@plt
+ 1.79% 1.45% lexer_benchmark lexer_benchmarks [.] decltype(auto) std::__1::__variant_detail::__visitation::__base::__dispatcher<1ul>::__dispatch[abi:ne190107]<std::__1::__variant_detail::__dtor<std::__1::__variant_detail::
+ 1.75% 1.46% lexer_benchmark libc.so.6 [.] __mpn_construct_double
+ 1.11% 1.11% lexer_benchmark libc.so.6 [.] __mpn_lshift
1.01% 0.58% lexer_benchmark lexer_benchmarks [.] memmove@plt
1.01% 0.88% lexer_benchmark libc.so.6 [.] round_away
+ 0.66% 0.00% lexer_benchmark [unknown] [.] 0x8000736e6f007300
0.64% 0.54% lexer_benchmark libc.so.6 [.] __strncmp_avx2
0.62% 0.44% lexer_benchmark libc++.so.1.0 [.] std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string()
+ 0.57% 0.00% lexer_benchmark [unknown] [.] 0xaa8000736e6f0000
0.55% 0.34% lexer_benchmark libc.so.6 [.] __errno_location
0.53% 0.34% lexer_benchmark lexer_benchmarks [.] std::__1::stod(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, unsigned long*)@plt
0.48% 0.19% lexer_benchmark libc++.so.1.0 [.] std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string()@plt
Here, I notice that we spend a lot of time in std::stod (to lex doubles). This is valuable
information. Stubbing the stod call with 0. gives over a 1.5x performance improvement. Some
research into stod shows that it does a lot, like locale handling, compliance, error checking,
etc. Since Tachyon is a relatively small language, I don't need a lot of those features (especially
the locales), so I can look into finding a replacement.
I found a few key options:
- fast_float::from_chars (header-only external library)
- std::from_chars (C++17 standard)
- strtod (C-style)
Of these options, I found fast_float to be the best mix of accuracy and performance. The difference in code is:
- constants.emplace_back(std::stod(std::string(start, current - start)));
+ double value;
+ auto answer = fast_float::from_chars(start, current, value);
+ if (answer.ec != std::errc())
+ errors.emplace_back(
+ Error::create(ErrorKind::LexError, SourceSpan(0, 0), "failed to read number"));
+ constants.emplace_back(value);
tokens.emplace_back(NUMBER, std::string_view(start, current), constants.size() - 1);
Which results in:
/home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
2025-07-14T10:00:54-04:00
Running /home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
Run on (8 X 2900.47 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x4)
L1 Instruction 32 KiB (x4)
L2 Unified 256 KiB (x4)
L3 Unified 6144 KiB (x1)
Load Average: 2.85, 4.96, 3.81
-------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------
LexerDataFixture/LexBlank 101 ns 101 ns 6235338
LexerDataFixture/LexBasic 281 ns 281 ns 2441141
LexerDataFixture/Lex1000Chars 6004 ns 5981 ns 104296
LexerDataFixture/Lex2000Chars 10140 ns 10105 ns 69330
LexerDataFixture/Lex5000Chars 41537 ns 41318 ns 19305
LexerDataFixture/Lex10000Chars 76672 ns 76398 ns 8880
LexerDataFixture/Lex20000Chars 150769 ns 150184 ns 4643
LexerDataFixture/LexBigComment 13869 ns 13829 ns 51221
Process finished with exit code 0
Which is a 56% improvement. Another optimization that I'd look into in the future is having it
parse strings without a . as an integer. I'll leave that for now, though, because I may or may
not decide to have native integer types in the virtual machine.
isalpha
Let's see another section of the perf results (I'm now using --call-graph=dwarf for a fuller call
graph).
- 1.56% tachyon::lexer::is_identifier_char(char const*) (inlined)
isalpha (inlined)
__ctype_b_loc@plt
__ctype_b_loc@plt is a hint that the program is reading the locale. Again, I don't want to
support locales for Tachyon, so I shouldn't need all of these details in is_identifier_char. So
let's try to move towards using something like this:
inline bool is_identifier_char(const char *ch) {
- return isalpha(*ch) || *ch == '_' || *ch == '\'';
+ return (*ch >= 'A' && *ch <= 'Z') || (*ch >= 'a' && *ch <= 'z') || *ch == '_' || *ch == '\'';
}
And I did the same for is_starting_identifier_char, is_starting_number_char and
is_number_char. This gives us:
- 0.00% lexer_benchmark lexer_benchmarks [.] tachyon::lexer::is_identifier_char(char const*) (inlined)
Which is a nice improvement from 1.56%. Something to look into in the future is using a lookup table for reading all of these characters instead.
- Conclusion
I analyzed my existing codebase to see where my initial implementation could benefit in
optimizations. I used tools like the standard std::vector::reserve, GNU's gperf, and the Linux
perf tool to optimize and better understand my program. In the end, I ended up with a 619% boost
in performance, and the program is now able to lex 20000 characters in just 0.135 milliseconds.
/home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
2025-07-14T12:09:32-04:00
Running /home/areg/projects/tachyon/cmake-build-release-benchmarks-only/libs/lexer/lexer_benchmarks
Run on (8 X 3100.2 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x4)
L1 Instruction 32 KiB (x4)
L2 Unified 256 KiB (x4)
L3 Unified 6144 KiB (x1)
Load Average: 1.72, 1.55, 1.49
-------------------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------------------
LexerDataFixture/LexBlank 113 ns 112 ns 6341061
LexerDataFixture/LexBasic 287 ns 286 ns 2402244
LexerDataFixture/Lex1000Chars 3662 ns 3650 ns 172662
LexerDataFixture/Lex2000Chars 6921 ns 6899 ns 102302
LexerDataFixture/Lex5000Chars 30090 ns 29996 ns 23380
LexerDataFixture/Lex10000Chars 71156 ns 70868 ns 9913
LexerDataFixture/Lex20000Chars 134971 ns 134296 ns 5357
LexerDataFixture/LexBigComment 17539 ns 17469 ns 39073
Process finished with exit code 0
Demonstration
I have implemented a lot of this language, and along with it many examples that can be found in the repository. For this demonstration, I'd like to focus on example 107-central-differences.tachyon. It's rudimentary, but looks like this:
// this program runs a layered classification network that identifies whether a fruit is poisonous
// by relating this value to two properties of the fruit: the size of its spots and the length of
// its spikes.
//
// this is the same as 106-with-lists.tachyon, but uses central differences to calculate the delta
// between weight and bias changes.
cost = fn (weights, biases, layer_data, layer_sizes, layer_amount, print) {
total_cost = 0;
x = 0;
y = 0;
while y < 30 {
while x < 30 {
layer_data = [x, y; 0, 0; 0, 0; 0, 0; 0, 0; 0, 0]
i = 1
while i <= layer_amount {
j = 1
while j <= layer_sizes[1, i+1] {
// sum all prev. data * weights + bias
total = 0
k = 1
while k <= layer_sizes[1, i] {
total = total + layer_data[i, k] * weights[k, (i - 1) * 2 + j]
k = k + 1
}
total = total + biases[j, (i - 1) * 2 + j]
layer_data[i+1, j] = 1 / (1 + 2.718 ^ (0-total));
j = j + 1
}
i = i + 1
}
// some hacks to get the cost value, left side and top side are 1 rest is 0
expectedVal = 0
input1copy = x
while (input1copy < 5) {
expectedVal = 1
input1copy = 999
}
input2copy = y
while (input2copy < 5) {
expectedVal = 1
input2copy = 999
}
print_copy = print
while print_copy == 1 {
output1copy = layer_data[layer_amount+1, 1]
output2copy = layer_data[layer_amount+1, 2]
while (output1copy >= output2copy) {
print("S ")
output1copy = 0-9999
}
condn = 0-9999
while (output1copy != condn) {
print("F ")
output1copy = 0-9999;
}
print_copy = 0
}
val1 = (layer_data[layer_amount+1, 1] - expectedVal) ^ 2
val2 = (layer_data[layer_amount+1, 2] - (1-expectedVal)) ^ 2
total_cost = total_cost + val1 + val2
x = x + 1
}
print_copy = print
while print_copy == 1 {
print("\n")
print_copy = 0
}
x = 0
y = y + 1
}
return total_cost
}
// some weights and biases to start with
weights = [
1.0, 0-1.0, 1.0, 0-1.0, 1.0, 0-1.0, 1.0, 0-1.0, 1.0, 0-1.0;
0-1.0, 1.0, 0-1.0, 1.0, 0-1.0, 1.0, 0-1.0, 1.0, 0-1.0, 1.0]
biases = [
0-5.0, 0-5.0, 0, 0, 0, 0, 0, 0, 0, 0;
0-5.0, 0-5.0, 0, 0, 0, 0, 0, 0, 0, 0]
// start with just the input data, and the shadow of the real data. note how the shadow follows the
// form of the layer sizes.
// TODO: eventually, we won't even need the shadow of the real data
// TODO: for now this is rectangular since the matrix constructor assumes a rectangular matrix. but
// i should really make this have any variation of sizes.
layer_data = [0, 0; 0, 0; 0, 0; 0, 0; 0, 0; 0, 0]
// each element is a layer's height, excl. the input layer
layer_sizes = [2, 2, 2, 2, 2, 2]
// number of layers, excl. the input layer
layer_amount = 5
// the learning rate
learning_rate = 0.005
// delta h with which to test slope
h = 0.00001
print("starting cost: ")
gen = cost(weights, biases, layer_data, layer_sizes, layer_amount, 1)
print(gen)
print("\n")
gens = 1
while gens <= 100 {
i = 1
while i <= layer_amount {
// 20 weights
j = 1
while j <= 2 {
k = 1
while k <= 10 {
weights[j, k] = weights[j, k] - h
gen = cost(weights, biases, layer_data, layer_sizes, layer_amount, 0)
weights[j, k] = weights[j, k] + h + h
gen' = cost(weights, biases, layer_data, layer_sizes, layer_amount, 0)
delta = gen - gen'
slope = delta / (2 * h)
weights[j, k] = weights[j, k] + slope * learning_rate
k = k + 1
}
j = j + 1
}
// 20 biases (?)
j = 1
while j <= 2 {
k = 1
while k <= 10 {
gen = cost(weights, biases, layer_data, layer_sizes, layer_amount, 0)
biases[j, k] = biases[j, k] + h
gen' = cost(weights, biases, layer_data, layer_sizes, layer_amount, 0)
biases[j, k] = biases[j, k] - h
delta = gen - gen'
slope = delta / h
weights[j, k] = weights[j, k] + slope * learning_rate
k = k + 1
}
j = j + 1
}
i = i + 1
}
gen = cost(weights, biases, layer_data, layer_sizes, layer_amount, 1)
print("current cost: ")
print(gen)
print("\n")
gens = gens + 1
}
This program is accurate and runs 13x faster than the pure python equivalent!
What's next
Some next steps for improving the performance of the lexer are:
- Parse integers separately
- Constant lookup table for
is_identifier_charand friends - Look into other kinds of list storage structures or memory allocators
Otherwise, I would love to work on some more language features, a foreign function interface, and a graphical library.