Moving stuff around in the sources, trying to get an idiom right on how to handle shared pointers in C++, adjusting a provisional lexer such that it will scan a test language, rethinking qualified names and namespaces, thinking about how to represent the parser right in C++. Unmotivating tidbits of engineering.
Type checker in the back of my mind. I am rethinking the ideas I had five years ago. I re-realize that there probably is no subtyping relation on types. Any concrete type can implement multiple interfaces at some given point, and that may be extended. Once extended, all old type derivations remain unchanged. Simply put: any concrete type just gets augmented with a list of interfaces and simple HM inferencing is enough. (Though it would be nice to put arbitrary values sharing interfaces in a container.) Moreover, since the language lacks assignment, there shouldn't be the need for a monomorphism restriction a la ML. Of course, you might want to store stuff at the module level but I would be perfectly happy labeling primitive routines for that 'unsafe'.
Since the language has exceptions I actually think I want RAII too. Which means 'destructors' on garbage collected nodes referring open files, database connections, etc. Which means I am inclined to drop my copying garbage collector and simply reference count everything. Even more wasteful but I can use C++ shared pointers for that and get thread safety for free. Moreover, good enough since the language guarantees that you're rewriting a DAG anyway.
It's just a trivial language. The lack of use cases means I shift between the semantics. If I read on a programmer's language I want to go into the direction of representing bytes, words, longs, etc.; if I read up on scripting languages my mood changes and I want to go for BigInts. Whatever, or not?
Friday, May 8, 2015
Log 050815
So no programming today. Family business, which was a good idea since I am easily mentally worn-out these days. But it remains in my mind.
So, a few random thoughts:
Trading robustness for speed. I rewrite everything to an eager manner of rewriting combinators to avoid stack overflows. I am not sure people willing to use the language will appreciate the, rather enormous, cost in running speed. If I am going to compile to LLVM, or some other JIT since LLVM seems to have performance problems, it becomes 'trivial' to encode to standard function calls. Moreover, a rather large number of optimizations would become feasible, or could easily be done by the LLVM compiler.
But you sacrifice robustness, and after a myriad of stack overflows bootstrapping in OCaml I don't want to go back.
As a side note, I also figured out how to handle closures in that scenario. The function knows when to reduce or push a closure so that's trivial. Exception handling is also enabled by default in the modern processor ABIs so I figure I don't even need to do difficult stuff for that.
Who will ever use this language? This is really a difficult question. FP enthusiasts? They already have a large number of compilers to chose from. SML or Haskell probably will gobble all of them up. I thought numeric computing for a moment, but those favor simple algorithmic languages. I read up on Julia, these programmers think fundamentally different than functional programmers and want optimal speed. I thought about web servers, which may be a target for some people, in which case I need to think about handling threads and shared state. Then I thought that it might be nice to write simple robust GUI applications; but who would choose my language over Python? I need a convincing use case. Some place where you need ridiculously robust software?
But I still like the language.
Compiling to Object files and C/C++ integration. Given a JIT compiling to .o files is automated for you, which is something I really want. But how the hell does the linker know about namespaces? Is there some name mangling going on I don't know about? Answer here, it seems.
So, a few random thoughts:
Trading robustness for speed. I rewrite everything to an eager manner of rewriting combinators to avoid stack overflows. I am not sure people willing to use the language will appreciate the, rather enormous, cost in running speed. If I am going to compile to LLVM, or some other JIT since LLVM seems to have performance problems, it becomes 'trivial' to encode to standard function calls. Moreover, a rather large number of optimizations would become feasible, or could easily be done by the LLVM compiler.
But you sacrifice robustness, and after a myriad of stack overflows bootstrapping in OCaml I don't want to go back.
As a side note, I also figured out how to handle closures in that scenario. The function knows when to reduce or push a closure so that's trivial. Exception handling is also enabled by default in the modern processor ABIs so I figure I don't even need to do difficult stuff for that.
Who will ever use this language? This is really a difficult question. FP enthusiasts? They already have a large number of compilers to chose from. SML or Haskell probably will gobble all of them up. I thought numeric computing for a moment, but those favor simple algorithmic languages. I read up on Julia, these programmers think fundamentally different than functional programmers and want optimal speed. I thought about web servers, which may be a target for some people, in which case I need to think about handling threads and shared state. Then I thought that it might be nice to write simple robust GUI applications; but who would choose my language over Python? I need a convincing use case. Some place where you need ridiculously robust software?
But I still like the language.
Compiling to Object files and C/C++ integration. Given a JIT compiling to .o files is automated for you, which is something I really want. But how the hell does the linker know about namespaces? Is there some name mangling going on I don't know about? Answer here, it seems.
Thursday, May 7, 2015
Log 050715
So combinator parsing in C++ turns out to be a bad idea, not only is it near impossible to get right most C++ implementations need several seconds for large files whereas the 'dumb' method usually finishes in milliseconds, needed to unlearn the functional style. Back to straight-forward recursive descent with switches on the current token.
I think I got how to write a far more trivial type checker than what I once started on. Straightforward Hindley-Milner with a subtyping relation on types implementing interfaces. It shouldn't be hard, I am also dropping all other ideas I had. Bottom-up HM+subtyping is good enough.
I've got a start of an AST description now going. Going to reference count everything through the use of C++ smart pointers; that's a bit overkill but it beats thinking about copying.
For downcasting it's going to be tag based switches followed by a static cast.
Everything is of course far more verbose than the bootstrap which I am using as a reference implementation.
So far so good. Making progress but this is probably still the easy part.
ADDENDUM: I thought I would share the AST solution I chose for people who might have ended up here by chance.
1) Define tags for your expressions:
typedef enum {
VAR,
ABSTRACTION,
APPLICATION,
} expr_t;
2) Define a base object:
class Expr;
typedef std::shared_ptr<Expr> ExprPtr;
class Expr {
public:
Expr(expr_t t): _tag(t) {};
expr_t tag() { return _tag; }
virtual ExprPtr clone() = 0;
private:
expr_t _tag;
}
3) Derive variants, for instance
class ExprApplication: public Expr {
public:
ExprApplication(ExprPtr e0, ExprPtr e1): Expr(APPLICATION), _left(e0), _right(e1) {}
ExprApplication(const ExprApplication& e) {
_left = e._left;
_right = e._right;
}
ExprPtr clone() {
return ExprPtr(new ExprApplication(*this));
}
// etc.
private:
ExprPtr _left;
ExprPtr _right;
}
It took a lot of googling but the idiom for using shared pointers where you mostly only reference the base class is aptly described here.
Well, that's it. Don't create cycles, as long as your term is a DAG, most likely it will always be a tree, this scheme should work with RAII and might even allow for memory-compact solutions far better than simply copying everything.
ADDENDUM: I thought I would share the AST solution I chose for people who might have ended up here by chance.
1) Define tags for your expressions:
typedef enum {
VAR,
ABSTRACTION,
APPLICATION,
} expr_t;
2) Define a base object:
class Expr;
typedef std::shared_ptr<Expr> ExprPtr;
class Expr {
public:
Expr(expr_t t): _tag(t) {};
expr_t tag() { return _tag; }
virtual ExprPtr clone() = 0;
private:
expr_t _tag;
}
3) Derive variants, for instance
class ExprApplication: public Expr {
public:
ExprApplication(ExprPtr e0, ExprPtr e1): Expr(APPLICATION), _left(e0), _right(e1) {}
ExprApplication(const ExprApplication& e) {
_left = e._left;
_right = e._right;
}
ExprPtr clone() {
return ExprPtr(new ExprApplication(*this));
}
// etc.
private:
ExprPtr _left;
ExprPtr _right;
}
It took a lot of googling but the idiom for using shared pointers where you mostly only reference the base class is aptly described here.
Well, that's it. Don't create cycles, as long as your term is a DAG, most likely it will always be a tree, this scheme should work with RAII and might even allow for memory-compact solutions far better than simply copying everything.
Wednesday, May 6, 2015
I mean like: Wow
I've been reading the entire Internet trying to track down how people write compilers, more precisely, parsers and abstract transformers in C/C++.
One post claimed the roughly the following: Compiler construction is a well-defined and finished trivial field where grammar theory is fully developed and compiler writers know very well how to implement the various aspects of compilers.
Then I read various sources, even GCC and CLang. The parsing compiler theory, roughly described as: bottom-up parsing is the most general and serves all your needs, has been abandoned for hand-written PEG recursive descent parsers with infinite lookahead; moreover, all pretty LALR and other compiler generation tools have been abandoned and rot away. Nobody knows what the best manner of representing an AST in C++ is and people patch the weirdest solutions together to write recursive descent parsers. I haven't even started on semantic analysis.
If all problems are solved then all the code I just read doesn't show it.
One post claimed the roughly the following: Compiler construction is a well-defined and finished trivial field where grammar theory is fully developed and compiler writers know very well how to implement the various aspects of compilers.
Then I read various sources, even GCC and CLang. The parsing compiler theory, roughly described as: bottom-up parsing is the most general and serves all your needs, has been abandoned for hand-written PEG recursive descent parsers with infinite lookahead; moreover, all pretty LALR and other compiler generation tools have been abandoned and rot away. Nobody knows what the best manner of representing an AST in C++ is and people patch the weirdest solutions together to write recursive descent parsers. I haven't even started on semantic analysis.
If all problems are solved then all the code I just read doesn't show it.
Monday, May 4, 2015
Term Rewriting using the Visitor pattern ist Verboten
I've been looking into what the correct manner of downcasting is in C++. Unfortunately, downcasting is only allowed on pointer or reference types and then in a manner I don't really like yet.
Still, I need it. Rewriting a term is best done functionally for various reasons. If you cannot imagine why, and I've seen a lot of people advising against it, and you propose a visitor pattern then you're an idiot.
Not convinced you're an idiot? Even Stroustrup agrees on that.
Which is the problem with the Internet these days. It used to be that I could google or post a question to a mailing list and get an informed answer within a few hours. These days, with widespread programming education, the Internet is full of idiots and bad advice.
So I am not sure what the proper manner of downcasting terms in C++ is. Another small puzzle.
Still, I need it. Rewriting a term is best done functionally for various reasons. If you cannot imagine why, and I've seen a lot of people advising against it, and you propose a visitor pattern then you're an idiot.
Not convinced you're an idiot? Even Stroustrup agrees on that.
Which is the problem with the Internet these days. It used to be that I could google or post a question to a mailing list and get an informed answer within a few hours. These days, with widespread programming education, the Internet is full of idiots and bad advice.
So I am not sure what the proper manner of downcasting terms in C++ is. Another small puzzle.
Sunday, May 3, 2015
Rewriting Terms in C++?
Given that I am now writing a compiler in C++ the question of term rewriting and following RAII comes up. A typical scenario following RAII is given below.
Say your simple term language has elements like this:
class AppTerm: public Term {
public:
AppTerm(const Term &l, const Term &r): _left(l), _right(r) {};
Term left() {
return _left;
}
Term right() {
return _right;
}
private:
Term _left;
Term _right;
}
Then, since most compilers do a lot of term rewriting, you'll have an enormous amount of code like:
Term rewrite_app(const AppTerm &t) {
SomeResult r0 = rewrite_term(t.left());
SomeResult r1 = rewrite_term(t.right());
// do something
return AppTerm(r0.value(), r1.value());
}
If I get C semantics right, then r0 and r1 will go out of scope and the resulting term will receive copies of whatever was constructed in the rewrite of both constituents of an application term.
I say it again: copies. Think about that for a moment. Copies.
If every copying rewrite function is going to copy entire terms for each intermediate step, as I think it will do when implemented naively, you're going to have one hell slow compiler.
Did I get C++ semantics right? The only way out is to use smart pointers it seems.
Say your simple term language has elements like this:
class AppTerm: public Term {
public:
AppTerm(const Term &l, const Term &r): _left(l), _right(r) {};
Term left() {
return _left;
}
Term right() {
return _right;
}
private:
Term _left;
Term _right;
}
Then, since most compilers do a lot of term rewriting, you'll have an enormous amount of code like:
Term rewrite_app(const AppTerm &t) {
SomeResult r0 = rewrite_term(t.left());
SomeResult r1 = rewrite_term(t.right());
// do something
return AppTerm(r0.value(), r1.value());
}
If I get C semantics right, then r0 and r1 will go out of scope and the resulting term will receive copies of whatever was constructed in the rewrite of both constituents of an application term.
I say it again: copies. Think about that for a moment. Copies.
If every copying rewrite function is going to copy entire terms for each intermediate step, as I think it will do when implemented naively, you're going to have one hell slow compiler.
Did I get C++ semantics right? The only way out is to use smart pointers it seems.
A C++ interpreter/compiler with an LLVM back-end
After an old-school bootstrap of the compiler I wrote, I decided I did it all wrong. Doing it right would imply creating an interpreter/compiler in C/C++.
The major lesson here: Despite academic enthusiasm, old-skool bootstrapping is awful and extending a bootstrap compiler is even more awful.
The major lesson here: Despite academic enthusiasm, old-skool bootstrapping is awful and extending a bootstrap compiler is even more awful.
The major hurdle I needed to take: my language is that good that I didn't feel like doing it all in C/C++ again.
Last week, I took the plunge. Starting on an interpreter/compiler in C++ which will hopefully have an LLVM back-end.
There go another few years of my life. I feel good about it.
Last week, I took the plunge. Starting on an interpreter/compiler in C++ which will hopefully have an LLVM back-end.
There go another few years of my life. I feel good about it.
Subscribe to:
Posts (Atom)