tgo Devlog #6: The Origin, the "Crack" of Hard Problems, and the Zero-Friction Threshold

go dev.to

There isn’t a singular, highly compelling, life-or-death reason I started building tgo (a TypeScript-to-Go native compiler). It didn’t start because I ran into a wall that could only be solved by compiling TypeScript down to a Go binary.

*It started because of a disappointment. *

A few weeks ago, I saw that Microsoft was releasing a Go compiler for TypeScript. I got incredibly pumped up. I thought, finally, a tool that will literally compile my TypeScript directly into native Go. But that isn't what it was. It turned out to be just a Go implementation of the TypeScript checker and the engine that converts it to standard JavaScript.

Don't get me wrong—that tool works flawlessly. It was a drop-in replacement that provided a monumental, instant speed improvement to my toolchain. As I update my libraries, I am immediately putting it in place everywhere. I cannot understand it enough, its fantastic.

But I was left wanting.

I couldn't stop thinking: Dude, it would be next-level awesome if this thing actually compiled into a native Go binary.

I started plotting the idea out. And then one day, I just decided: I’m just gonna do it.

Oh You Abstract Problems You...

To understand why I'm doing this, you have to understand how my brain works. I am not a fan of pure, isolated logic for the sake of it, but I absolutely love a good challenge. Specifically, mathematical, abstract, language-y, construction-type problems. For me, these puzzles are like crack. Once I walk into them, I have a very hard time dropping them.

I’ve always been this way. Back in high school chemistry, instead of sleeping in class, I spent a month trying to reverse-engineer a mathematical method to find the square root of any integer. I didn't have sophisticated math skills at the time (and probably don't now). I wasn't some brilliant kid reading math textbooks at home (skateboarding, playing punk music, and 40s was more important). But when all the noise stops, and I'm left with myself, these kind of problems, I can't put down. So I worked.

Eventually I found out that there really ISN'T a real way to solve it other than a sort of "guess" then change your number, then guess again, then guess again.

I did the same thing with Bitcoin years later. I wanted to know why it felt like I made less money when it was popular than when it was completely unknown. I ended up reverse-engineering the math and realizing that the formula for percent change was the key.

I developed a theorem from it that goes something like this:

Cornwell's Theorem

For every function that is less than x², the percent change over time always decreases with an increasing x.

As time goes on, the amount of actual growth, the velocity of your wealth, decreases. And to put this in perspective "x²" (true exponential growth) is completely fucking crazy. People use the word "exponential" all the time, but they don't mean it. Nothing in the real world is truly exponential on a long enough curve. It always changes its dynamics, to the downside. But recognizing what curve something is dictates the velocity of gains. (I'll write a separate post on this later, because it's a vital understanding just about everything physical in the universe and the prospects of growth).

I'm Just a Dude. I think?

I'm not a mathematician. I’m not a statistician. I don't have a PhD in physics. I don't even have a formal Computer Science degree, and none of my formal education is involved in the field where I have made all of money, or all of my professional reputation. I think people get a kick out of me having no formal training in software, and being a "boss-level" software guy.

But computer engineering is something I can do in my sleep, without even attempting it. I just have a natural intuition for working with these problem sets. My biggest criticism of the school system is that they never tell you why math is useful. It wasn't until Calc 2 in college, when I was on the precipice of failing with a straight F, that I realized, “Oh my god, we’re just finding the volume of a physical object.” I then proceeded to fail my first class ever in the topic I am strongest in, leave hard sciences in college, and get a checkbox degree in Sociology. (Perfect for understanding why the world is going crazy with all this over the top nonsense philosophies)

If they had anchored the abstract logic to real-world utility, I would have been hooked immediately. I'm rambling now to paint a picture but let's get back to tgo.

Runtime Interpreters Seem Like A Good Idea, But Are They?

I looked around before I started this to see if someone else had already built it. I keep worrying someone has. I found projects like Goja, which I initially panicked over, only to realize it’s just an engine for running JavaScript inside a Go application. That is not what I am doing.

I am a TypeScript developer because I want to stay as high-level as I possibly can. TypeScript has a syntax that absolutely smokes almost every imaginable language out there in terms of simplicity and usability.

Every time I look at Scala, Haskell, or C, the syntax gets in the way. I used to like Python, but I hate it now. The verbosity required to write simple shit like lambdas in Python is actually shocking to me (though I realize most Python writers today are data scientists, not software engineers, which is why when I read python I functions 1000 lines long, with 15 indentations). I would love to write in C, but the verbosity of doing something like lambdas or function passing is awful. Even compiled Go is wildly verbose.

I didn't choose Go because of its language skillz, but rather because its close enough to Typescript and can be compiled to a binary

I don't want to deal with static typed verbosity. Typescript is impressive with its ability to let type information flow along without having to explicitly declare it. If the checker can figure it out... why can't a compiler?

That brings me to the fundamental issue of modern interpreters. I wish Node would compile down right now. I don't know what is holding up this kind of work. I guess "because its Javascript".

At compile time, the machine knows what the types are. How do I know? Because the TypeScript Checker literally knows exactly what the types are! As long as you aren't doing janky, hyper-dynamic legacy JavaScript stuff, the compiler has the map. If it knows the types, it should output an optimized binary. Maintaining infinite, chaotic dynamicness at runtime..... can we move on now?

There has to be a middle ground between "you have to be so verbose you spend all your life doing it" and "you can never be optimized because we have to run things nobody should run, and maybe it'll change at runtime, even though 99.9999% of code doesn't."

The AWS Lambda Vision

At the end of the day, I love the idea of my applications running exponentially faster. But more than that, I want the massive reduction in file size and overhead.

I cannot wait to compile this son of a bitch down and run it in an AWS Lambda environment. Years ago on a project, we lived in Lambda. Eventually, we moved to AWS CDK and ESBuild from a hand rolled deployment library we created. ESBuild was a lifesaver because it did tree-shaking—when you import a specific file from Lodash, it only pulls what you actually use. And before that, we were always fighting to get the total file sizes down so that Lambda would allow it. "Oh, you added that one library, for a single function? Nope."

That automatically happens with Go native compilation. tgo parses the TypeScript, writes out the verbose Go code behind the scenes, and when it compiles to a binary, it natively strips out everything you didn't use. Ding!

My Ideal Toolchain:

  1. I write an SDK in TypeScript (defining public interfaces, enums, types, and an Axios client).
  2. I write the backend and frontend in TypeScript, both using that SDK.
  3. tgo compiles the backend directly into an ultra-fast, tiny Go binary for the server.
  4. The frontend runs through standard ESBuild for the browser.
  5. The SDK can be turned into OpenAPI specs to automatically build clients in any other language.

It would be a drop-in replacement for ESBuild on the backend, but potentially much better.

I'll Kill This Project If There is Friction

As much as I love working tgo, there is a hard line where I will kill the project.

Right now, while it is 100% usable, I have some hard limitations to punch through. I need argparse working perfectly, because it is the absolute best argument parser in existence (exact Linux standard, flawless commands capability). I need lodash, and date-fns. Once I get those, I'll start compiling my own reusable libraries for small production runs.

I "know" I'll have to write thin Go-wrapper layers for things like Mongo database drivers rather than trying to compile massive JS database libraries.

But here is the ultimate test: Friction.

I have a really high standard for the things I create. I don't like it when shit doesn't work. I absolutely fucking hate building processes that make me do things more than once, require continuous monitoring, or demand endless maintenance. I refuse to waste my time. If contracts I hop on get to that, I might just have to grow a beard and move out to the mountains and plant fruit trees.... (oh wait...)

If I can get tgo to the point where it works silently behind the scenes—I write my normal TS, run my unit tests effortlessly, compile it, and drop a Go binary into production—I will yell about it from the mountaintops. I will use it everywhere.

But today isn't that day

But the day the compilation process or the unit testing becomes too much of a pain in the ass to maintain? I will drop this project instantly.

I am building this to eliminate friction, not create it. If I can release this out into the wild and other engineers iterate on it, we can create something pretty awesome for backends.

Until then, I'm going to keep knocking out one syntax error at a time.

(Look how readable this error is....:)

1) library-argparse-2-0-1-compile
     Go library build failed: # argparse_v2_0_1
     index.go:569:24: cannot range over __tgo_runtime.CallMethod(__tgo_runtime.Get(__tgo_this, "_get_args"), __tgo_this) (value of type interface{})
     index.go:572:35: cannot range over __tgo_runtime.CallMethod(__tgo_runtime.Get(__tgo_this, "_get_kwargs"), __tgo_this) (value of type interface{})
     index.go:709:25: invalid operation: cannot index __param0 (variable of type any)
     index.go:710:24: invalid operation: cannot index __param0 (variable of type any)
     index.go:736:25: cannot use name (variable of type any) as string value in variable declaration: need type assertion
     index.go:737:22: cannot use name (variable of type any) as string value in variable declaration: need type assertion
     index.go:739:5: invalid operation: metavar += __tgo_runtime.CallValue(sub_pkg.Default, " (%s)", __tgo_runtime.CallMethod(__tgo_runtime.Get(aliases, "join"), aliases, ", ")) (mismatched types string and interface{})
Enter fullscreen mode Exit fullscreen mode

Source: dev.to

arrow_back Back to Tutorials