Make Your Own Programming Language
mattias-.github.io
mattias-.github.io
[1] http://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_H...
Regardless of my complaints, it was an interesting experience.
When looking for resources on making toy languages, most of them said not to bother with a parser and use a pre-built solution. I think they're probably wrong - the parser is actually pretty easy (relative to the rest of the process of getting a language working) and at a stretch, fun.
I think it's more flexible and maintainable this way. I didn't have to wrestle with odd grammar rules. Implementing the JS "automatic semicolon insertion" with parser generator tools seems like it would have been a cluster headache.
On the topic of precedence parsing, they have some really good properties in an incremental context: you can start parsing anywhere and the results will be the same. The problem is expressiveness, and while I came up with some hacks to push them beyond operators, pure recursive descent is much easier to work with (as I learned from Martin Odersky's scalac compiler).
That said, handwritten top down parsers are very tweakable and flexible which can be a big plus. In particular, error recovery and things like semicolon insertion. You also get more control over code size and parser performance.
The tools generate a monstrous, ugly mess that nobody understands, which is linked into your executable.
When it gets complicated, debugging it is a massage session, where you make changes that you don't entirely understand in hopes that a desired behavioral change comes about.
Here is a debugging fail: you cannot just put a break point on a rule, because it's not a function. The state machine is traversing multiple rules at the same time.
In a recursive-descent parser, you can break on a rule and get a call stack.
A bottom-up parser like that produced by LALR parser generator could make this slightly more awkward, depending on what you need to generate the bytecode, but an LL(k) parser generator (e.g. Antlr) should let you do most of what you'd do with recursive descent, and in a similar way.
(Asked here)
https://www.reddit.com/r/Compilers/comments/2o78hd/exist_a_s...
Also, I wonder how NOT lost the info that already have the AST in the bytecode conversion, I even have the crazy idea to store everything in a sqlite database, but I suspect will be slow (to read later in the VM...)
Things get a bit more wrinkly with control flow, but you can design bytecode to make it easier to rehydrate that too.
Parsers are a different kettle of fish entirely though. You hypothetically compare a handwritten parser with a "fast" generated parser. Presumably a handwritten parser will be faster than a "slow" generated parser?
If the language to be parsed is simple and has no ambiguities that need semantic information to resolve (this is not the case for C, for example, never mind C++), a generated parser may save time. Would it be faster than a handwritten parser? I have my doubts, for similar reasons as lexers. The automaton state is stored implicitly via the program counter, and state transitions are in code: similar arguments apply. If the handwritten parser is doing more busywork, such as a lot of recursion to handle precedence in expressions, then it may be slower. But if it uses precedence parsing just for operators, it can get that back, while not losing the benefits of recursive descent style.
OTOH, if you're trying to parse a fairly ambiguous language, you can save a substantial amount of effort with a parallelized LR parser, something that builds a forest of parse trees and discards options that are no longer feasible when more information comes in: a Tomita or GLR parser. Parsing such languages with recursive descent typically involves a lot of extra work building up semantic info as you go, or parsing a simpler form of the language and rewriting with disambiguation later, after further analysis. But GLR parsing is O(n^3) in the worst case. Hand-written may be a lot more work, but it might also be faster. It depends.
I'm doing one, and I'm only working on the AST.
I take the idea from this blog:
http://www.trelford.com/blog/post/interpreter.aspx
And from http://www.itu.dk/people/sestoft/plc/. So I get rid of the parsing, and go directly at "how do type checking?", "How I map vars to the environment?", "How make the debugger???", "So, I can have a AST for INTS, BOOLS... now how let the user create your own types???", "How make possible to do macros? I'm not a LISP!", "I'm on F#. How lift the stuff F# already have to do not doing it again???", "What to do: Erlang actors or GO CSP? and how?", "How do pattern matching", "Auto-instrument the code interpreter with dtrace or similar, and why I think this is good?", "I wanna be a reactive language?", "I decide the language be relational. I strongly believe that is amazing. I don't know yet why and how prove it" and a huge list like this...
I don't have a clean answer yet for some of this questions!
Also, I get rid of the idea of make a compiler, and do a interpreter instead. That is another distraction. I suspect that if later on I turn the interpreter to a bytecode, the step to full compilation will be simple, and without solve several questions about the full language the task of rewrite both the parsing and the compiler is not cool.
They are all separate parts, and you can do them in any order you want.
I like to think of the language as two parts - syntax and features. How do I want the language to look and what features do I want the language to have? The parser and lexer handle the syntax, and the AST handles the features.
This way, I can play around with the syntax of the language a little later.
I would start with a really simple language:
- add expressions, proper infix support for arithmetic
- variables
- if / else / else if
- scoping / functions
- loops if you don't want to just use recursion
then, you can get more advanced:
- lambda expressions / first class functions / closures
- namespaces or types
- macros. if you can make nice looking lisp-style macros for an infix language i would love you
write it as an interpreter, add an llvm backend.
It should be fun. do a little at a time.
I don't know about Rust, but Elixir, Dylan and Sweet.js have Scheme-style, pattern matching based macro systems while Nimrod is a bit unique in having procedural macros conceptually closer to defmacro from some Lisps.
There are also languages where functions can decide whether to evaluate their arguments or not, in the latter case making them work like macros. Io is an example of such a language.
Anyway, infix languages with nice macro systems do exist, it's just that none of them became popular enough (yet?). Also, programmers tend to fear macros for some reason (probably because of they are in C and similar languages) which makes having macros rather low-priority feature for language designers. But it's perfectly possible to create a (very nice!) macro system for infix language and it's been done.
The thing I'm now is how implement macros (and how deeply), not the syntax, is kinda easy to see the way with lisp, but still not with more "normal" syntax.
Also, I have tough (in a way to provide linq-like capabilities) if could be nice to have this = that (evaluate that and put on this) and this ::= that (get the AST on this and later evalualte this) and let decorate the functions with "fun(test:AST.LogicalOp)", so macros at runtime, not just compile time..
I'd suggest ignoring precedence rules completely here. It generally simplifies the grammar and parser considerably. It doesn't make the language unworkable, either, as long as you allow for grouping with parentheses - Smalltalk does this and it work rather well.
I'd also try working with libraries like PyParsing first before attempting to use mammoth size parser generators. In my experience, especially for simple grammars, this way you can be done with parsing stage with minimum effort and concentrate on the "features" side.
I wish that in the languages I have worked on that I could just ignore precedence and use parens, but operator precedence is expected.
This is true once you complete the project, but it seems like it can be dangerous: if you start off your mission to make a programming language by soldering components on a breadboard, say, then, you will learn something, to be sure, but you will probably get so embroiled in preliminaries that you will never learn what you intended to learn. (Of course, sometimes learning something other than what you intended is a good thing ….)
Are you perhaps using a browser or a DNS stack that rejects this type of domain name?
It's quite interesting... dig seems to resolve the domain just fine, but neither chrome, curl nor firefox seem to be able to open the page. (tested on Fedora/CentOS/Android)
However, curl and firefox seem to be RFC-compliant. RFC952 states: "The last character must not be a minus sign or period."
Edit: Google Chrome works.
randunel@18:~$ curl Mattias-.github.io
curl: (6) Could not resolve host: Mattias-.github.io
randunel@18:~$ dig +short Mattias-.github.io
github.map.fastly.net.
185.31.18.133
The exact wording is: "They must start with a letter, end with a letter or digit, and have as interior characters only letters, digits, and hyphen."
I just added a custom domain/CNAME to my github pages so it should be reachable at http://blog.ppelgren.se
/*
* Verify that a domain name uses an acceptable character set.
*/
/*
* Note the conspicuous absence of ctype macros in these definitions. On
* non-ASCII hosts, we can't depend on string literals or ctype macros to
* tell us anything about network-format data. The rest of the BIND system
* is not careful about this, but for some reason, we're doing it right here.
*/
#define PERIOD 0x2e
#define hyphenchar(c) ((c) == 0x2d)
#define underscorechar(c) ((c) == 0x5f)
#define bslashchar(c) ((c) == 0x5c)
#define periodchar(c) ((c) == PERIOD)
#define asterchar(c) ((c) == 0x2a)
#define alphachar(c) (((c) >= 0x41 && (c) <= 0x5a) \
|| ((c) >= 0x61 && (c) <= 0x7a))
#define digitchar(c) ((c) >= 0x30 && (c) <= 0x39)
#define borderchar(c) (alphachar(c) || digitchar(c))
#define middlechar(c) (borderchar(c) || hyphenchar(c) || underscorechar(c))
#define domainchar(c) ((c) > 0x20 && (c) < 0x7f)
int
res_hnok(const char *dn) {
int pch = PERIOD, ch = *dn++;
while (ch != '\0') {
int nch = *dn++;
if (periodchar(ch)) {
(void)NULL;
} else if (periodchar(pch)) {
if (!borderchar(ch))
return (0);
} else if (periodchar(nch) || nch == '\0') {
if (!borderchar(ch))
return (0);
} else {
if (!middlechar(ch))
return (0);
}
pch = ch, ch = nch;
}
return (1);
} /*
hnokpre.c: LD_PRELOAD shim to bypass valid hostname checking in glibc
gcc -fPIC -shared hnokpre.c -o libhnokpre.so
LD_PRELOAD=./libhnokpre.so curl http://mattias-.github.io/
*/
int res_hnok(const char* dn)
{
return 1;
}
int __res_hnok(const char* dn)
{
return 1;
} * Note the conspicuous absence of ctype macros in these definitions. On
* non-ASCII hosts, we can't depend on string literals or ctype macros to
* tell us anything about network-format data. The rest of the BIND system
* is not careful about this, but for some reason, we're doing it right here.
Well that is only a problem if the charset is converted to something else. Are there still systems out there without a C compiler that can be told to use ASCII?Also working on JavaScript native compiler + developing a new language on top as subset for JavaScript: https://github.com/InfiniteFoundation/dopple (front page info is outdated though).
Most of the concepts are pretty simple - it's when you get into if/while/for statements. Especially if you have nested if/while/for statements. Conceptually easy to implement using recursion but during the compilation phase keeping track of where you are at can be...difficult. I'll be the first to admit my implementation sucked but it worked.
I've been working on Python-like systems language with a compiler in Python targeting LLVM IR, and it's been cool (though so much work to get a halfway usable language!).
https://github.com/djc/runa if anyone's interested.
EDIT: s/is/looks/. I meant the syntax.
A lot of language enthusiasts just winced.
I submitted my own posts to HN also, but I did not get the same attention as you did, congratulations!
I'm looking forward to the next iteration ;)
It is actually a transcompilation to Bash from a functional language, using Python for the intermediate processing.
It would be cool with a DSL to generate low level code like X86 ASM, LLVM IR or JVM bytecode. That would be so meta!
The pain is compared to the absolute and total simplicity of defining a grammar when you're in a whitespace-insensitive setting (parser generators are easy to express and efficient in deterministic grammars).
We have a lot of good parsing tools out there, but we might be missing the right tools for building out good lexers