#viper
1 messages · Page 2 of 1
thanks
i need to stop putting random tuples and pairs
hm, still getting the same linker issues
annoyingly i cant use objdump on windows
why not? the one in msys2 works just fine
and also llvm-readelf for seeing the symbols/relocs
it just says format not recognized
possibly because the obj is completely fucked
what if you do file on the object
does msys2 have that?
yes
hold on
ok i have some weird version of those programs
that isnt msys2
ill install it now
also one funny thing I found is that if the input file doesn't exist you are going to get an empty object file as the output (well not completely empty but with only the header and no sections)
what is the issue that you get
what about mingw gcc
let me find out
just installing base-devel
ah mingw objdump actually works
i get undefined reference to `no symbol'
heres llvm-readelf output
thats weird, there are literally no symbols in that
yeah
what is the asm that it was generated from?
message: db "Hello World!\0"
extern OutputDebugStringA
main:
mov rcx, message
call OutputDebugStringA
mov rax, 0
ret
ah, one issue is the lack of a short name for that symbol
afaict, theres no handling for anything other than a short name
oh yeah
for (const auto& sym : mSymbolTable) {
PEWrite(stream, sym.mShortName.name, 8);
PEWrite(stream, sym.mValue);
PEWrite(stream, sym.mSectionNumber);
PEWrite(stream, sym.mType);
PEWrite(stream, sym.mStorageClass);
PEWrite(stream, sym.mNumberOfAuxSymbols);
}
``` i assume something here?
yep
yeah if its larger than 8 bytes the first 4 bytes are zeroes and then the following 4 contain a string table offset
ah you've got the union in there already
actually wait it should be supported
if (name.size() < 8)
{
memcpy(symbol.mShortName.name, name.c_str(), name.size() + 1);
}
else
{
symbol.mShortName.offset.zeros = 0;
symbol.mShortName.offset.offset = mStringTable.size();
mStringTable += name + '\0';
}
yes i just realized that
thats the entire point of the union
yea
unless some padding is being added to the struct?
looks like there are indeed 2 bytes of padding at the end
ill just make it a uint64_t
but that padding shouldn't really affect anything as the struct isn't being straight up written to the file
it kinda is
bcs we are treating it as a char[8]
still no symbols either way
the symbol names are in the file, so i dont know why its not showing any
it looks to be somehow related to the long symbol name because even if you have just ```x86asm
extern OutputDebugStringA
main:
ret
At the beginning of the COFF string table are 4 bytes that contain the total size (in bytes) of the rest of the string table. This size includes the size field itself, so that the value in this location would be 4 if no strings were present.
no
no we dont
let me add that now
should be easy enough
PEWrite(stream, static_cast<uint32_t>(mStringTable.size() + 4));
ok that fixed it
did you also change the offset
size_t usableStart = symbolTableOffset + symbolTableSize + mStringTable.size();
ah
ill do that now
also needed to change it in the symbol index into the strtab
that looks better
nice, it segfaults
my guess is that its something to do with the relocation
well that makes sense as the call instruction only expects a 4 byte relative relocation
so doing a 8 byte absolute relocation is going to overwrite something after it (and not really be right for the call either)
mVirtualAddress is only 32-bits
ah i see we are using IMAGE_REL_AMD64_ADDR64
nasm emits a IMAGE_REL_AMD64_REL32 there
yeah the relocation type should actually depend on the instruction
hm
thats a bit annoying
especially abstracting that
i can maybe just get away with passing a parameter which is absolute or relative
that looks better
though the first relocation name is .text from nasm and not message
yea, i think thats something we are missing
symbol table entries for sections
today i will try to flesh out the type system a bit more
add void type and pointer types
i now have if statements
adding more operators today, ie < > >= <=
ok i finished adding all of those
func @main() -> i32 {
let testvar: i32 = 1;
while(testvar <= 13) {
testvar += 1;
}
return testvar;
}``` about the limit of what i can do currently
but still progress
i will probably add function calling next
which is something ive been avoiding
because of the complexity it adds to regalloc
im not sure whether i want to just throw a value into a new register if i encounter something that needs a specific register, or if i want to allocate around that
I guess the optimal solution would be to somehow reconsider the previous register allocations for the operands/a variable that is using some kind of result register if you encounter something like that
but it might not be that simple to do
the simplest solution would probably be to just spill the registers for the duration of the instruction and do moves to them before the instruction
yeah that would be simplest
i was thinking about some kind of tracking of which registers are going to be required
then not allocate them for any values that are live ever at the same time as the values requiring those registers
but that isnt the easiest thing
yeah
then there are even more things you could do to optimize it further like if there are other long living variables and you don't have any other registers for them than the specific needed ones it might still make sense to put them to there and spill them for the duration of the other variable/move the other variable to some other register at some point
but I wouldn't worry about things like this to begin with
i will have very little if any time to do anything today
so if i do have some time i'll probably just add some more operators or something
wont be doing anything tonight, im too tired
nvm^^
func @test() -> i32 {
return 69;
}
func @main() -> i32 {
let testvar: i32 = 1;
while(testvar <= test()) {
testvar += 1;
}
return testvar;
}``` i am able to do this now
one thing though is i dont think it would work if you call multiple functions that have overlapping intervals
as it would try to put both of them into eax
so i need to find a solution to that
not sure what i want to do with the 30 minutes i have to work on this tonight
we have pointers now
func @test() -> i32 {
return 69;
}
func @main() -> i32 {
let testvar: i32 = 1;
while(testvar <= test()) {
testvar += 1;
}
let ptr: i32* = &testvar;
return *ptr;
}```
neat
question: why * after type not before
is it?
* before is easier
because let a: i32* = &testvar can be parsed as let a: i32 *= &testvar
nah
thats irrelevant
*= is special in the same way != and ++ are
doesnt matter
if ur grammar is wrong with something like
let <symbol>: <type> <assignment>
it doesn't matter because both have the same sort of semantics
++ is different from + +
just as *= is different from * =
and let a: i32 *= lol isnt even legal anyway
ok fair
hmmmmmm
yes because i parse the type then check for star tokens immediately after
and set the type to a pointertype to the previous one
type =
gettype(currentTokenString)
while currentToken is star
type = pointertype(type)
its not much easier
but a bit
this is fixed now
i will add arguments next
ok i added arguments to vipir
now i will add them to the lang itself
then parameters for call expressions
func @main(argc: i32) -> i32 {
let testvar: i32 = argc;
let ptr: i32* = &testvar;
return *ptr;
}``` thats done
the codegen is horrendous
added parameters to vipir
func @max(a: i32, b: i32) -> i32 {
if (a > b) return a;
return b;
}
func @main(argc: i32) -> i32 {
return max(argc, 5);
}``` most advanced viper program
the build times for the entire project are getting a bit out of hand
ok you can now declare and call external functions, so you can print an integer by calling a C function
i will add strings next
for some strange reason this assembly: x86asm 5d: 48 c7 45 f8 00 00 00 mov QWORD PTR [rbp-0x8],0x0 64: 00 65: b8 00 00 00 00 mov eax,0x0 6a: c9 leave 6b: c3 ret is getting linked to this assembly x86asm 11bd: 48 c7 45 f8 60 11 00 mov QWORD PTR [rbp-0x8],0x1160 11c4: 00 11c5: 00 00 add BYTE PTR [rax],al 11c7: 00 00 add BYTE PTR [rax],al 11c9: 00 c9 add cl,cl
ive got a feeling my relocations are off
ah yeah i think thats it
wasn't able to get that done tonight, there are some more deep rooted issues i will need to look into
i might just make global things always be PIE
i dont really see any reason not to
it should make relocations and suchlike a lot simpler
think i've found some issues with how im doing the relative offsets
fixed that
next thing i need to add is SIB, which ive kinda been avoiding
ok that wasnt as bad as i initially thought
though the next thing is to add it to vipir for pointer arithmetic, which might be a bit more involved
added -> operator
added global variables
one thing im not exactly sure how i want to do is pointer addition for struct pointers
since you cant just use the scale part of sib
what gcc seems to do is multiply the index to the number of fields then multiply that by the alignment
i also need to stop throwing everything into the text section
added the data section and now globals other than functions are put in it
im debating what the syntax should be for struct initializing
something with { ... } but i feel like a prefix to that would make it a bit clearer
maybe something like struct foo { ... }
just a personal opinion but I hate that :p for the same reason that I hate using non-typedef'd structs in c, imo its mostly just useless prefixes for the sake of it when you can easily guess what it is from the usage of it (or just use an ide and look it up there)
im also coming at it from an ease-of-parsing perspective
{ ... } would be ambiguous between a compound statement and an initializing block in that case
and i guess i could just make it depend on if its coming after an assignment or not but i dont really like that kind of context-sensitive stuff
how would foo {...} be ambiguous?
sorry i thought you just meant { ... }
yeah i guess i could do that
ill have a think
yeah
the build times are getting a bit out of hand
i think not too long from now i will be able to begin writing viperos
maybe a week or two
the big things i need to implement are arrays and type casting
added pointer casts
struct abc {
test: i64;
foo: i32;
}
global a: struct abc = struct abc { 11, 33 };
func @main() -> i8 {
let ptr: i32* = &a.foo;
return *(i8*)ptr;
}```
the next thing i want to implement is casts of integral types to other integral types
then integral -> pointer and vice versa
added casts from smaller to larger integral types
now i need truncation
done that also now
last bit of casting for the time being done, which is ptr -> int and int -> ptr
arrays also done now
arrays as in slices?
or as in c-style pointers
also why is global separate from let?
seems a bit weird
and also why struct tags?
they are there in C to ensure unambigous parsing
and even then not really
as in C style arrays
i dont know actually
ill allow them to be declared inside functions in future though
so it would be ambiguous then unless i have some kind of static keyword
you can do a bit of a meme with constexpr semantics i think
hmm, not for mutable statics
ive just realized my parsing logic for vasm doesnt work across the board
due to the fact that there are instructions that take one or two operands
or three even
continuing on from #1095033465440841799, im rewriting the regalloc
because its bad
function @main() -> i32 {
1:
alloca VREG1: i32
store i32 12 -> i32* VREG1
load i32 %VREG2, i32* VREG1
imul %VREG3, %VREG2, i32 1
store %VREG3 -> i32* VREG1
load i32 %VREG2, i32* VREG1
ret %VREG2
}``` so far i have this kind of virtual reg allocation
obviously if that alloca is in a register the loads can be eliminated
one thing im not sure about here is
lets say the alloca is put in rax
then vreg2 is rcx, vreg3 is rdx
since vreg2 isnt needed, the loads can be omitted
but then vreg3 should be vreg2
because vreg2 isnt used for anything
i guess i can leave that for now
right
now i need to deal with precolored thingies
and function calls destroying everything
the way ive done this wont work too well with that
i kinda need to break the 1:1 translation of vreg to physical reg/stack slot
because rax would be usable before and after a function call, so long as the value isnt live over it
but currently i'd have to make that reg never used
hm i found another annoying bug
previously if an alloca was in a register, loads from that alloca would do nothing and instead just use that register directly
but in a special case where that is the last time that alloca was used, the register would then be available
and be reused by another value
i might just make alloca's live for the entire function for the time being
albeit a bit of a hack
another issue i've found is if i have spilled a value to the stack, but i need to store something from memory into it
like in theory if i had a loadinst that had been spilled, that was loading a spilled alloca
it would attempt to do mov [rbp-off1], [rbp-off2]
which is obviously not possible
im working on a lower level IR that the main IR compiles to
it'll let me do optimizations much easier i think
LABEL main
LABEL 1
MOVE $69 -> %T1
MOVE %T1 -> %T2
MOVE %T2 -> eax
RET```
like this has a bunch of easy optimizations that can be done
currently just porting over all of the existing IR to the new lower level IR instead of generating asm directly
then ill work on some kind of isel layer
im not sure how i want what will eventually become lea to work in this IR
i would also need to force the thing its taking the address of to actually be in memory
ok thats done
next thing is either some optimizations or loading a value from an address stored in like a register
which im not entirely sure how i want to do either
i cant just check if its a register since an alloca could be in a register
i guess i can just have a different ir instruction for indirect loads and the main loadinst resolves which one to emit
if its an alloca or not
i think its just mul now left to do
ok new backend is all done
i managed to break everything by changing the liveness analysis
but i think i may have fixed it for the most part
no
im still fixing everything
as in making it work at all
let alone optimizing it
the current issue is when i try to gep from the stack back onto the stack
as opposed to what?
just inlining pointer math
as in to get a struct member from struct
well yeah but why getelementptr
like in general
you dont have to be a sheep
actually its because strict aliasing i think
pointer cast and add?
i guess
so if we have ```c
struct foo {
int a;
char b;
short c
};
foo x;
x.a; // (int*)(&x)+0```
essentially this?
yeah pretty much i think
i guess that works
this may be slightly worse due to strict aliasing, which i think is why llvm doesnt do that
but i would still need special logic for pointer adding
not really?
just multiply by scale beforehand
yeah
yourself
oh
hm
this would require slightly more work to turn it into like [rax+8] or whatever but i guess thats fine
kinda?
i mean yeah it makes instruction selection a bit harder
theres a simple fix
just use arm :^)
understandable
noone wants to use arm
tbh this isnt even that bad
its pretty simple isel stuff
yeah i guess
i am bad though
ok tomorrows job will be to remove gep then
and replace it
ok now i will attempt to start adding some proper optimizations
i added some optimizations
they are not amazing by any means
but its definitely a step in the right direction
did you do load-store forwarding yet
hm
i dont know what the real name for it is
the idea is that you combine *X = Y and Z = *X and turn it into Z = Y
and you can completly discard the stores if nobody reads from the alloca
eh kinda
you should cse too tbh
oh and the reason gep is useful is because of strict aliasing
you know two addresses a1 and a2 do not alias if you obtained them by accessing different fields in one struct
iiuc
alias analysis
is that a particularly useful optimization?
well its like half of that thing so yes
fair
i have discovered a bug
if you need to pass a series of parameters to a function
but some of the values to pass are in a different parameter register
they will get trashed
ok im gonna move the regalloc layer to the LIR
i have discovered a very subtle bug
and im not entirely sure how i want to go about fixing it
if a register is smashed, any active values using that register must have their register switched to a different one
but it just searches for registers from the pool of free registers
however, one of those may be used previously in the value's lifetime
for some other value
which would cause them to use the same register at the same time
which obviously cannot happen
https://discord.gg/ahTsCwFZ viper discord for anyone who would like to join
fixed a bunch of issues in the compiler
we got vscode syntax highlighting
its pretty basic
just the shitty textmate thing
but eventually ill write a language server
language servers seem so fun
if you want to write one for viper be my guest 
unfortunately they contain one of the hard problems of computer science (cache invalidation)
i think the LSP makes it much easier
not specifically what you just said but the whole thing in general
this is getting quite difficult to debug without debug info(or at the very least backtraces) so im going to attempt to implement some portion of DWARF
wow it has been a while
currently rewriting as per usual
this time with a focus on things actually working
most importantly the type system
so we have implicit casting and such from the get go
the current state is quite basic, although we do have function pointers and some conditional stuff
func main(argc: i32) -> i8 {
let f: (i8)* -> i32 = &test;
let fp: (i8)** -> i32 = &f;
let g: (i8)* -> i32 = *fp;
return g(argc);
}``` heres some example code
func thing(test: i32) -> i32 {
return test;
}
func thing(test: i8) -> i32 {
return test;
}
func main() -> i32 {
let test2: i16 = 3;
let test3: i8 = 12;
test3 = 2;
let test: i32 = thing(test2);
if (false)
test = test2 + test3;
let ptr: i32* = &test;
return -*ptr;
}``` and some more
gonna work on writing some tests today
a combination of fuzzing, end-to-end and unit testing
ok parser tests are done
also added extern functions
im debating now how i want to handle character types and strings
obviously i could just have i8 also be a char
and i8* for string
considering rewriting the IR as well because it is really bad
viper is no longer dead(again)
just finished implementing the module system
export import example.test2;
export func x() -> i32 {
return y();
}``` some example code, should all be pretty self-explanatory

some of classes finished tonight
you can define classes and use them as a type, but no member access yet
so they dont do a huge amount
today if time permits i will be adding member access to classes
which needs slightly more work than i would have liked due to how variables exist inside the compiler
i think for now structs will only be allocad instead of being ssa
mem2reg probably cant do much with them at the moment either
but thats something i can deal with later
after member access i'll make it so you can actually import/export classes
which should be fairly straightforward
then if i have time, member functions
ok member access was actually relatively easy to implement
not sure how i want to handle exported classes that have fields of types that are not exported
something like this ```
class foo {
...
}
export class bar {
foo thing;
}```
to someone importing this file, bar is visible but foo wont be
ok we have exporting classes
was relatively painless
not sure what i want to do next
either member functions or some kind of symbol file emission for libs
decided i will actually be adding namespaces
we have basic namespaces now
they dont function in imported files just yet
but i want to redesign the import system anyway
better import system is done
we have namespace lookups for types now
so now im just gonna add a shit ton of small simple features like enums, using declarations etc etc
member functions are a thing now
they have implicit this parameter
but you can also just type the name of a member
we also have access modifiers now
well just one actually
everything defaults to private but you can put public in front
im not sure if i want a c++ style public: or not
enums also done now
added cast expressions, the syntax is just cast<X>(y)
going to attempt to add templates next
we have basic templated functions
taking a small break from viper to add a compiler for the IR language so i can work on some more optimizations
me when this project
what on earth does this mean
iykyk
no
i see
Viper is the stage name of Houston-based hip hop artist Lee Carter who has gained a cult online following for his unconventional style of rapping and prolific output of records, with the most notable release being his 2008 rap album You'll Cowards Don't Even Smoke Crack.
New Viper Video F**k Earth Im Gone Wage An Interstella War
https://discord.gg/Pehw34A
https://outsiderrecords.bandcamp.com/album/f-k-earth-im-gon-wage-an-interstella-war
Stream at https://open.spotify.com/album/18yW8EBZUdq3XVRzrkn21t
made a little lexer generator since i basically have the same lexer written 3 times now
now all the projects will use the lexer generator
its not exactly a generator ig, more just configuring the lexer
but i'll extend it in future
doing a bit more vlex(lexer generator) stuff today
a friend of mine is using it for one of their projects and theres a few issues to iron out so ill be doing that first
then adding some qol features
after that ill go back to vipirc and get that going
then probably spend a bit more time doing optimizations
finally fixed all the vlex errors
how much progress has been made since 2023?
probably quite a lot
why do you ask?
ive rewritten the compiler probably multiple times since 2023
just curious, when I click the channel that's the first thing that shows
been working on a slightly different project recently, its basically just viper but simplified and with a slightly less garbage compiler
func main() -> i32 {
let x: str = "hello";
let array: char[5];
let y: str = array[0:5];
bmemcpy(y, x);
test(x);
test(y);
return 0;
}``` some example code
has its own build system and library format