ginger/NOTES
2017-07-11 18:52:07 -06:00

424 lines
21 KiB
Plaintext

I need to prioritize the future of this project a bit more. I've been thinking
I'm going to figure this thing out at this level, but I shouldn't even be
working here without a higher level view.
I can't finish this project without financial help. I don't think I can get a v0
up without financial help. What this means at minimum, no matter what, I'm going
to have to:
- Develop a full concept of the language that can get it to where I want to go
- Figure out where I want it to go
- Write the concept into a manifesto of the language
- Write the concept into a proposal for course of action to take in developing
the language further
I'm unsure about what this language actually is, or is actually going to look
like, but I'm sure of those things. So those are the lowest hanging fruit, and I
should start working on them pronto. It's likely I'll need to experiment with
some ideas which will require coding, and maybe even some big ideas, but those
should all be done under the auspices of developing the concepts of the
language, and not the compiler of the language itself.
#########
Elemental types:
* Tuples
* Arrays
* Integers
#########
Been doing thinking and research on ginger's elemental types and what their
properties should be. Ran into roadblock where I was asking myself these
questions:
* Can I do this without atoms?
* What are different ways atoms can be encoded?
* Can I define language types (elementals) without defining an encoding for
them?
I also came up with two new possible types:
* Stream, effectively an interface which produces discreet packets (each has a
length), where the production of one packet indicates the size of the next one
at the same time.
* Tagged, sort of like a stream, effectively a type which says "We don't know
what this will be at compile-time, but we know it will be prefixed with some
kind of tag indicating its type and size.
* Maybe only the size is important
* Maybe precludes user defined types that aren't composites of the
elementals? Maybe that's ok?
Ran into this:
https://www.ps.uni-saarland.de/~duchier/python/continuations.htm://www.ps.uni-saarland.de/~duchier/python/continuations.html
https://en.wikipedia.org/wiki/Continuation#First-class_continuations
which is interesting. A lot of my problems now are derived from stack-based
systems and their need for knowing the size input and output data, continuations
seem to be an alternative system?
I found this:
http://lambda-the-ultimate.org/node/4512
I don't understand any of it, I should definitely learn feather
I should finish reading this:
http://www.blackhat.com/presentations/bh-usa-07/Ferguson/Whitepaper/bh-usa-07-ferguson-WP.pdf
#########
Ok, so I'm back at this for the first time in a while, and I've got a good thing
going. The vm package is working out well, Using tuples and atoms as the basis
of a language is pretty effective (thanks erlang!). I've got basic variable
assignment working as well. No functions yet. Here's the things I still need to
figure out or implement:
* lang
* constant size arrays
* using them for a "do" macro
* figure out constant, string, int, etc... look at what erlang's actual
primitive types are for a hint
* figure out all needed macros for creating and working with lang types
* vm
* figure out the differentiation between compiler macros and runtime calls
* probably separate the two into two separate call systems
* the current use of varCtx is still pretty ugly, the do macro might help
clean it up
* functions
* are they a primitive? I guess so....
* declaration and type
* variable deconstruction
* scoping/closures
* compiler macros, need vm's Run to output a lang.Term
* need to learn about linking
* figure out how to include llvm library in compiled binary and make it
callable. runtime macros will come from this
* linking in of other ginger code? or how to import in general
* comiler, a general purpose binary for taking ginger code and turning it
into machine code using the vm package
* swappable syntax, including syntax-dependent macros
* close the loop?
############
I really want contexts to work. They _feel_ right, as far as abstractions go.
And they're clean, if I can work out the details.
Just had a stupid idea, might as well write it down though.
Similar to how the DNA and RNA in our cells work, each Context is created with
some starting set of data on it. This will be the initial protein block. Based
on the data there some set of Statements (the RNA) will "latch" on and do
whatever work they're programmed to do. That work could include making new
Contexts and "releasing" them into the ether, where they would get latched onto
(or not).
There's so many problems with this idea, it's not even a little viable. But here
goes:
* Order of execution becomes super duper fuzzy. It would be really difficult to
think about how your program is actually going to work.
* Having Statement sets just latch onto Contexts is super janky. They would get
registered I guess, and it would be pretty straightforward to differentiate
one Context from another, but what about conflicts? If two Statements want to
latch onto the same Context then what? If we wanted to keep the metaphor one
would just get randomly chosen over the other, but obviously that's insane.
############
I explained some of this to ibrahim already, but I might as well get it all
down, cause I've expanded on it a bit since.
Basically, ops (functions) are fucking everything up. The biggest reason for
this is that they are really really hard to implement without a type annotation
system. The previous big braindump is about that, but basically I can't figure
out a way that feels clean and good enough to be called a "solution" to type
inference. I really don't want to have to add type annotations just to support
functions, at least not until I explore all of my options.
The only other option I've come up with so far is the context thing. It's nice
because it covers a lot of ground without adding a lot of complexity. Really the
biggest problem with it is it doesn't allow for creating new things which look
like operations. Instead, everything is done with the %do operator, which feels
janky.
One solution I just thought of is to get rid of the %do operator and simply make
it so that a list of Statements can be used as the operator in another
Statement. This would _probably_ allow for everything that I want to do. One
outstanding problem I'm facing is figuring out if all Statements should take a
Context or not.
* If they did it would be a lot more explicit what's going on. There wouldn't be
an ethereal "this context" that would need to be managed and thought about. It
would also make things like using a set of Statements as an operator a lot
more straightforward, since without Contexts in the Statement it'll be weird
to "do" a set of Statements in another Context.
* On the other hand, it's quite a bit more boilerplate. For the most part most
Statements are going to want to be run in "this" context. Also this wouldn't
really decrease the number of necessary macros, since one would still be
needed in order to retrieve the "root" Context.
* One option would be for a Statement's Context to be optional. I don't really
like this option, it makes a very fundamental datatype (a Statement) a bit
fuzzier.
* Another thing to think about is that I might just rethink how %bind works so
that it doesn't operate on an ethereal "this" Context. %ctxbind is one attempt
at this, but there's probably other ways.
* One issue I just thought of with having a set of Statements be used as an
operator is that the argument to that Statement becomes.... weird. What even
is it? Something the set of Statements can access somehow? Then we still need
something like the %in operator.
Let me backtrack a bit. What's the actual problem? The actual thing I'm
struggling with is allowing for code re-use, specifically pure functions. I
don't think there's any way anyone could argue that pure functions are not an
effective building block in all of programming, so I think I can make that my
statement of faith: pure functions are good and worthwhile, impure functions
are.... fine.
Implementing them, however, is quite difficult. Moreso than I thought it would
be. The big inhibitor is the method by which I actually pass input data into the
function's body. From an implementation standpoint it's difficult because I
*need* to know how many bytes on the stack the arguments take up. From a syntax
standpoint this is difficult without a type annotation system. And from a
usability standpoint this is difficult because it's a task the programmer has to
do which doesn't really have to do with the actual purpose or content of the
function, it's just a book-keeping exercise.
So the stack is what's screwing us over here. It's a nice idea, but ultimately
makes what we're trying to do difficult. I'm not sure if there's ever going to
be a method of implementing pure functions that doesn't involve argument/return
value copying though, and therefore which doesn't involve knowing the byte size
of your arguments ahead of time.
It's probably not worth backtracking this much either. For starters, cpus are
heavily optimized for stack based operations, and much of the way we currently
think about programming is also based on the stack. It would take a lot of
backtracking if we ever moved to something else, if there even is anything else
worth moving to.
If that's the case, how is the stack actually used then?
* There's a stack pointer which points at an address on the stack, the stack
being a contiguous range of memory addresses. The place the stack points to is
the "top" of the stack, all higher addresses are considered unused (no matter
what's in them). All the values in the stack are available to the currently
executing code, it simply needs to know either their absolute address or their
relative position to the stack pointer.
* When a function is "called" the arguments to it are copied onto the top of the
stack, the stack pointer is increased to reflect the new stack height, and the
function's body is jumped to. Inside the body the function need only pop
values off the stack as it expects them, as long as it was called properly it
doesn't matter how or when the function was called. Once it's done operating
the function ensures all the input values have been popped off the stack, and
subsequently pushes the return values onto the stack, and jumps back to the
caller (the return address was also stored on the stack).
That's not quite right, but it's close enough for most cases. The more I'm
reading about this the more I think it's not going to be worth it to backtrack
passed the stack. There's a lot of compiler and machine specific crap that gets
involved at that low of a level, and I don't think it's worth getting into it.
LLVM did all of that for me, I should learn how to make use of that to make what
I want happen.
But what do I actually want? That's the hard part. I guess I've come full
circle. I pretty much *need* to use llvm functions. But I can't do it without
declaring the types ahead of time. Ugghh.
################################
So here's the current problem:
I have the concept of a list of statements representing a code block. It's
possible/probable that more than this will be needed to represent a code block,
but we'll see.
There's two different ways I think it's logical to use a block:
* As a way of running statements within a new context which inherits all of its
bindings from the parent. This would be used for things like if statements and
loops, and behaves the way a code block behaves in most other languages.
* To define a operator body. An operator's body is effectively the same as the
first use-case, except that it has input/output as well. An operator can be
bound to an identifier and used in any statement.
So the hard part, really, is that second point. I have the first done already.
The second one isn't too hard to "fake" using our current context system, but it
can't be made to be used as an operator in a statement. Here's how to fake it
though:
* Define the list of statements
* Make a new context
* Bind the "input" bindings into the new context
* Run %do with that new context and list of statements
* Pull the "output" bindings out of that new context
And that's it. It's a bit complicated but it ultimately works and effectively
inlines a function call.
It's important that this looks like a normal operator call though, because I
believe in guy steele. Here's the current problems I'm having:
* Defining the input/output values is the big one. In the inline method those
were defined implicitly based on what the statements actually use, and the
compiler would fail if any were missing or the wrong type. But here we ideally
want to define an actual llvm function and not inline everytime. So we need to
somehow "know" what the input/output is, and their types.
* The output value isn't actually *that* difficult. We just look at the
output type of the last statement in the list and use that.
* The input is where it gets tricky. One idea would be to use a statement
with no input as the first statement in the list, and that would define
the input type. The way macros work this could potentially "just work",
but it's tricky.
* It would also be kind of difficult to make work with operators that take
in multiple parameters too. For example, `bind A, 1` would be the normal
syntax for binding, but if we want to bind an input value it gets weirder.
* We could use a "future" kind of syntax, like `bind A, _` or something
like that, but that would requre a new expression type and also just
be kind of weird.
* We could have a single macro which always returns the input, like
`%in` or something. So the bind would become `bind A, %in` or
`bind (A, B), %in` if we ever get destructuring. This isn't a terrible
solution, though a bit unfortunate in that it could get confusing with
different operators all using the same input variable effectively. It
also might be a bit difficult to implement, since it kind of forces us
to only have a single argument to the LLVM function? Hard to say how
that would work. Possibly all llvm functions could be made to take in
a struct, but that would be ghetto af. Not doing a struct would take a
special interaction though.... It might not be possible to do this
without a struct =/
* Somehow allowing to define the context which gets used on each call to the
operator, instead of always using a blank one, would be nice.
* The big part of this problem is actually the syntax for calling the
operator. It's pretty easy to have this handled within the operator by the
%thisctx macro. But we want the operator to be callable by the same syntax
as all other operator calls, and currently that doesn't have any way of
passing in a new context.
* Additionally, if we're implementing the operator as an LLVM function then
there's not really any way to pass in that context to it without making
those variables global or something, which is shitty.
* So writing all this out it really feels like I'm dealing with two separate
types that just happen to look similar:
* Block: a list of statements which run with a variable context.
* Operator: a list of statements which run with a fixed (empty?) context,
and have input/output.
* There's so very nearly a symmetry there. Things that are inconsistent:
* A block doesn't have input/output
* It sort of does, in the form of the context it's being run with and
%ctxget, but not an explicit input/output like the operator has.
* If this could be reconciled I think this whole shitshow could be made
to have some consistency.
* Using %in this pretty much "just works". But it's still weird. Really
we'd want to turn the block into a one-off operator everytime we use
it. This is possible.
* An operator's context must be empty
* It doesn't *have* to be, defining the ctx which goes with the operator
could be part of however an operator is created.
* So after all of that, I think operators and blocks are kind of the same.
* They both use %in to take in input, and both output using the last statement
in their list of statements.
* They both have a context bound to them, operators are fixed but a block
changes.
* An operator is a block with a bound context.
##############@@@@@@@@@#$%^&^%$#@#$%^&*
* New problem: type inference. LLVM requires that a function's definition have
the type specified up-front. This kind of blows. Well actually, it blows a lot
more than kind of. There's two things that need to be infered from a List of
Statements then: the input type and the output type. There's two approaches
I've thought of in the current setup.
* There's two approaches to determining the type of an operator: analyze the
code as ginger expressions, or build the actual llvm structures and
analyze those.
* Looking at the ginger expressions is definitely somewhat fuzzy. We can
look at all the statements and sub-statements until we find an
instance of %in, then look at what that's in input into. But if it's
simply binding into an Identifier then we have to find the identifier.
If it's destructuring then that gets even *more* complicated.
* Destructuring is what really makes this approach difficult.
Presumably there's going to be a function that takes in an
Identifier (or %in I guess?) and a set of Statements and returns
the type for that Identifier. If we find that %in is destructured
into a tuple then we would run that function for each constituent
Identifier and put it all together. But then this inference
function is really coupled to %bind, which kind of blows. Also we
may one day want to support destructuring into non-tuples as well,
which would make this even harder.
* We could make it the job of the macro definition to know its input
and output types, as well as the types of any bindings it makes.
That places some burden on user macros in the future, but then
maybe it can be inferred for user macros? That's a lot of hope. It
would also mean the macro would need the full set of statements
that will ever run in the same Context as it, so it can determine
the types of any bindings it makes.
* The second method is to build the statements into LLVM structures and
then look at those structures. This has the benefit of being
non-ambiguous once we actually find the answer. LLVM is super strongly
typed, and re-iterates the types involved for every operation. So if
the llvm builder builds it then we need only look for the first usage
of every argument/return and we'll know the types involved.
* This requires us to use structs for tuples, and not actually use
multiple arguments. Otherwise it won't be possible to know the
difference between a 3 argument function and a 4 argument one
which doesn't use its 4th argument (which shouldn't really happen,
but could).
* The main hinderence is that the llvm builder is really not
designed for this sort of thing. We could conceivably create a
"dummy" function with bogus types and write the body, analyze the
body, erase the function, and start over with a non-dummy
function. But it's the "analyze the body" step that's difficult.
It's difficult to find the types of things without the llvm.Value
objects in hand, but since building is set up as a recursive
process that becomes non-trivial. This really feels like the way
to go though, I think it's actually doable.
* This could be something we tack onto llvmVal, and then make
Build return extra data about what types the Statements it
handled input and output.
* For other setups that would enable this a bit better, the one that keeps
coming to mind is a more pipeline style system. Things like %bind would need
to be refactored from something that takes a Tuple to something that only
takes an Identifier and returns a macro which will bind to that Identifier.
This doesn't *really* solve the type problem I guess, since whatever is input
into the Identifier's bind doesn't necessarily have a type attached to it.
Sooo yeah nvm.