Cool idea! It seems like you are trying to extend the whole "optional types" idea of Typescript further to add optional low level programming. One thing that came to mind. One of the major reasons why Typescript's optional/gradual typing system works so well is because the the type system and the runtime logic are mostly orthogonal. All the types that are used and declared in typescript, are stripped away by the compiler and don't affect the javascript logic. This means that if I import some other team's typescript code, I can freely write javascript on top without worrying.
Low-level operations like manual memory management are not like that. They are part of the runtime, and _do_ affect the business logic. It's a leaky abstraction. So if I import some other team's Rust code and I try to write RustScript on top, I might have to worry about some special edge cases where the argument I pass in isn't properly garbage collected or what not.
As far as I can tell, D already implements this idea. The Rust/RustGC duality is very similar to D's approach, which has manual memory management and an optional GC [1], and DMDScript [2] could be the scripting language.
Common lisp can be and has been successfully used for high-performance numerics[0] as well as low-level system code[1], all while being a high-level, garbage-collected language. (The latter property likely leading to better overall performance in large programs.)
Verification may be interesting, for high-assurance (e.g. automotive/aerospace) applications. It's for this reason that a theorem prover[2] and an ml[3] have been implemented, and they interoperate freely with the rest of common lisp.
In other words: we are already where you want to be, and we are even less stratified than you propose we would need to be.
For example, Rust used to have a garbage collector. A language set boundary would be a good way to add this back in without requiring a runtime at the lower levels.
Rust already has “levels”, such as no-std or core.
I would argue that libraries especially need highly restricted language subsets. E.g.: an image parser library should be “pure” in the sense of never being permitted to access system APIs. Image binary in, decoded bitmap out. Like a pure function, but at the module level.
Not sure if you're familiar with or heard about AssemblyScript [1]. It sounds like the goal you are trying to achieve, but with a saner approach: create a TypeScript-like language that compiles to WebAssembly. This means it gives you native access to SIMD, and eventually even threads! All while avoiding the overhead of learning a new language (well there are still some gotchas since it is actually a new language, but very close).
No need to think about whether you need to optimize X function or manually manage memory somewhere, and still get the awesome performance of Wasm.
Yep, but it doesn't really solve any of the problems that I mention in the article, unfortunately. The Typescript familiarity part is nice, but it doesn't really work well with existing code; you still have to set up Wasm-JS communication; it doesn't work with existing ArrayBuffers / multiple memories; it's garbage collected if you want to use more complex objects; etc. In many ways it's better to use Rust (which looks a bit like Typescript) than AssemblyScript..
Yeah, AssemblyScript is a key WASM language variant - I made a list of some of the other TypeScript-like implementations last year. Static TypeScript and TypeScriptCompiler do something similar in order to run inside web pages. At a quick glance, this proposal might get some ideas from the thinking done on BorrowScript
I've tried AS for a while but then just learned Rust. The wasm ecosystem is a lot more mature on Rust (probably the best of all languages) so I'm glad i'v learned it. AS syntax looks like TS, but under the hood it's another language with a lot of open problems like testing, sending data back and forth between wasm and js. AS supports operator overloading, but only for for values of the same time, like Vector * Vector, but not BiVector * Vector, and Rust does.
Hi! Author here. Really curious what you all think. It's a pretty crazy idea but maybe there's something to it! Hacker News is always a special place for ideas like this — you never know what you're gonna get ;) — so I'm looking forward to discussing this with y'all!
The TypeScript++ example looks like the type of code that JITs typically like. Python has Numba, where users affix a @jit annotation to functions to request that. It works pretty well in my experience.
However, at this point, I'm mostly of the opinion that langs need some notion of mechanical sympathy if they want to be used for high-performance. JS engines probably have tens of millions of dollars of dev effort poured into them by now, yet they get soundly beaten by WASM, which is much younger. The conceptual foundation matters a lot.
That said, there's probably a nice space for an ergonomic, reasonably fast PL that sits between Rust and JS.
Your example really clicked with me, having written a substantial quantity of JS in a "memory managed" way to avoid GC, and avoiding memcpy by passing buffers in place of pointers.
My initial instinct is that putting such considerations behind a preprocessor syntax has the disadvantage of making them more opaque and magic, and that I personally would continue to do such things more explicitly - which I realise is an ironic thing to say about an untyped, interpreted language, it's probably just the little pseudo "grey beard" on my shoulder which occasionally whispers silly things about "building their own computer out of rocks and twigs".
I guess the point is that there's a balance to the cost vs benefit: making it more convenient (what you call ergonomics), making it more accessible to those that were less likely to manually use that technique, and perhaps resulting in an average of more performant code; but at the cost of more obscurity, and more "magic", perhaps resulting in less educated programmers.
I'm probably overthinking it. Ultimately this argument even applies to for loops, but it's worth being conscious of that cost - it's likely a good idea.
I know you've responded to a few people on here that Assemblyscript doesn't address the main issues you're talking about in the article, but honestly it does have a lot of things going for it.
I'd love to see a more direct breakdown of this space that explains why prior works like assemblyscript, nectarjs, walt, etc all fall short, and where an ideal solution would need to do better.
^^ this is the type of thing I'd love to chat about if you're interested btw
Why not write Rust directly, and target WASM for client side environments?
I support efforts to tune existing tech stacks. But there is such a more direct path to performance, reliability, etc etc by dropping (alt)JS entirely now that anything can compile to WASM.
"Why can't we have a language that compiles to highly optimized JS?" is definitely something that has crossed my mind, especially in situations where I reached for faking structs with TypedArrays. As soon as that gets more complicated than something like an array of x,y,z points things get difficult (not to mention almost impossible to debug) really fast.
I suspect that if you could come up with an easy way to express a large tree really efficiently in a TypedArray in a relatively generic way, that you already will do better than most attempted solutions here.
The main feedback I have is that I wanted to read this near the end of my lunch break, because I've been interested for years in the idea that a reasonably-restricted subset of TypeScript (who needs all that dynamism) can almost definitely be compiled to machine code and be made super fast, faster than V8 -- so I was looking for a TLDR in this article and spent a couple minutes looking for it and then just reading whole paragraphs and still don't really know what you've got here.
You might want to reconsider the ++ naming convention. Many people capable of a critical appraisal of C++ wouldn't want an association with C++. The prejudice that your name triggers is that you don't understand what others think C++ got wrong, and you're likely to be making the same mistakes.
It doesn't look like you're making the same mistakes, but why trigger that prejudice?
These two lines are worth looking at because they showcase why I am not really fond of Rust as much as people who would rewrite everything from the Linux kernel to the IMF in Rust if they could (which in itself is seriously off putting if my tone didn't tell you that):
type Vec2 = { x: number, y: number };
function avgLen(vecs: Vec2[]): number
versus:
struct Vec2 { x: f64, y: f64 }
fn avg_len(vecs: &[Vec2]) -> f64 {
There has been more than two decades between the release of Perl and Rust and you haven't learnt anything? Wow. Listen: code written in languages heavy with sigils and shorthand are hard to understand, read and most importantly: maintain. This adds a mental load which is -- as clearly visible from above -- is totally unnecessary. I have no idea why would anyone in 2010, several years after the famous "memory is the new disk, disk is the new tape" do this to save on characters in the source code. The teletype is a distant memory.
I know nothing about rust, but what are you actually complaining about here?
The only additional "sigil" in the rust example is the & for vector which I assume marks a reference. Languages that confuse objects with references are a pet peeve of mine, so good for rust to mark the difference. : vs -> and type[] vs [type] are just preferences, neither is obviously better. The Vec2 declaration in the first example actually has more sigils than the rust example (= and ;).
Regarding shorthands, just number would be unacceptable for 99% of the work I do, and it would be something that specifies signedness, integer vs float and size. As a c++ programmer, having to write std::uint64_t gets old very quickly and I'm happy that rust uses a shorter name for these.
I don't think that using a shorthand for something very common is an hindrance to comprehension. Do you think one should write "have not" instead of "haven't" in English?
> Wow. Listen: code written in languages heavy with sigils and shorthand are hard to understand, read and most importantly: maintain.
As someone who maintains a 145k line Rust codebase all on his own, I have to hard disagree on this one.
Once you get used to the syntax it just blends into the background and becomes a non-issue.
> This adds a mental load which is -- as clearly visible from above -- is totally unnecessary.
Totally unnecessary? Okay, so how would you do the following with the sigilless syntax (and disambiguate between them):
- Specify a unique reference? (`&T` is shared, `&mut T` is unique)
- Specify a lifetime? (`&'a T` is a reference to T with lifetime `a`)
- Specify a raw pointer? (`*const T` and `*mut T` are const/non-const raw pointers)
- Specify that the argument is moved into the function? (just the raw type `T`)
People have been trying to come up with a better syntax for Rust, and AFAIK so far nobody has been able to do it. There's always a tradeoff somewhere that ends up making matters worse than what we have now.
If you view Rust as a high-level language, then yes, there's probably a slight overload of sigils, operators and characters. However, Rust was not outright designed to replace languages like Python, Java or JavaScript but systems level languages like C++. Given that is has no runtime and a fair few compile time guarantees, as a programmer you need to tell the compiler explicitly what something is and how it is owned. If you come from a language with a runtime and start using Rust as if it is another high-level language you will hit the borrow checker wall at some point.
You could also write the function above as `fn avg_len(vecs: Vec<Vec2>) -> f64 {`, and I personally would write the TS one as `const avgLen = (vecs: Array<Vec2>): number`. However, there's a difference between the ownership of a `Vec` and `&[]`, something a language like TS doesn't need to worry about but Rust does.
Perl was a vastly more complex language than Rust. It was literally trying to build something with the complexity and ad-hoc features of natural language out of random symbols. It's not surprising that it didn't work very well.
The more complex the underlying semantics you're working with, the more verbose and less symbol-driven your language should be. You can see this clearly with things like COBOL, which tried to integrate every feature that might ever be needed for business programming within its base language - a staggering amount of complexity. English-like syntax kept that workable.
> Various articles have been written, comparing the performance of Javascript versus WebAssembly ("Wasm") in the browser.
Nit pick: Every word in this sentence in the article is a link. This way of linking multiple related resources is so hard to keep track of and navigate. I need to hover over or click each word to discover the resource.
I just middle click to open each link in a new tab. I can read the new tab, close it, leave it open, whatever. The links I've already clicked will turn purple, meaning I can easily see which links have been visited already.
That syntax of JS bugs me the most. The “in” keyword could be scrapped and have a .keys() on the object but alas back-compat.
Mnemonic: of = objects fail, in = index names
Yeah yeah objects can be arrays and iterable but you know what I mean. Best I could do! If you dont like just use the “in” mnemonic and Sherlock Holmes.
We briefly toyed with the reverse idea; wanted to share some code between web and native, and TypeScript / C++ already look very similar - what if we invented a subset of TypeScript that can be simply translated into C++ (as long as you stick to static use and types).
Worked ok for a while but broke down real quick once we brought in external npm dependencies.
Now we're just embedding V8 in our native apps and running the TS code through that. Not as interesting, but lets us import any random npm package (which is both good and bad).
I thought WASM was considered slower than JavaScript only for the use cases where heavy usage of DOM is necessary. Since that's not available through WASM it needs to make a call to a JS function to perform the DOM manipulation and that incurs extra overhead.
I think these cases will just never be good for WASM. DOM manipulation heavy Apps (which is a good chunk of JS code out there) may continue to be in JS and only the CPU intensive tasks would be computed in WASM.
A feature that Haxe offers could help here. Abstracts are compile time only types of which the implementation is fully inlined. Meaning we could define an abstract over ArrayBuffer which has an iterator of abstracts over Float64Array which define the x and y property getters. Once compiled the code will look very similar to the example given. It's one of the things I miss the most in TypeScript, coming from haxe.
Sounds like BorrowScript; which is TypeScript syntax, a Rust borrow checker, and Go-like coroutines. It's designed for wasm and web api targets. (not compatible with TypeScript though)
I've never written (or even read) JS code with ArrayBuffers. I wonder how much mileage you might get with better education and/or libs for using them? And perhaps a highly opinionated linter for things like optimal for-loop patterns?
Sorry, are we saying that Typescript is as fast as Rust in general, or when Rust is compiled to WASM, or what? I am confused and don't know enough about the problem to figure it out myself.
The older I get the less I care about "expressive power" and "speed of development". I don't view these things as an advantage most of the time, actually often the opposite. The price is always paid later on, and it is high.
[+] [-] xixixao|3 years ago|reply
[+] [-] woojoo666|3 years ago|reply
Low-level operations like manual memory management are not like that. They are part of the runtime, and _do_ affect the business logic. It's a leaky abstraction. So if I import some other team's Rust code and I try to write RustScript on top, I might have to worry about some special edge cases where the argument I pass in isn't properly garbage collected or what not.
[+] [-] creativemonkeys|3 years ago|reply
[1] https://dlang.org/spec/garbage.html
[2] https://github.com/DigitalMars/DMDScript
[+] [-] moonchild|3 years ago|reply
Verification may be interesting, for high-assurance (e.g. automotive/aerospace) applications. It's for this reason that a theorem prover[2] and an ml[3] have been implemented, and they interoperate freely with the rest of common lisp.
In other words: we are already where you want to be, and we are even less stratified than you propose we would need to be.
0. https://github.com/marcoheisig/Petalisp
1. https://github.com/froggey/mezzano/
2. https://www.cs.utexas.edu/users/moore/acl2/
3. https://github.com/coalton-lang/coalton
[+] [-] jiggawatts|3 years ago|reply
For example, Rust used to have a garbage collector. A language set boundary would be a good way to add this back in without requiring a runtime at the lower levels.
Rust already has “levels”, such as no-std or core.
Similarly C# has a scripting variant: https://visualstudiomagazine.com/articles/2021/06/14/csharp-...
And it also supports AoT compilation to a single EXE, but there were a lot of limitations. The latest attempt is a WIP: https://github.com/dotnet/runtime/issues/61231
I would argue that libraries especially need highly restricted language subsets. E.g.: an image parser library should be “pure” in the sense of never being permitted to access system APIs. Image binary in, decoded bitmap out. Like a pure function, but at the module level.
[+] [-] qpafqx|3 years ago|reply
[+] [-] 3np|3 years ago|reply
Python+mypy+cython
https://cython.readthedocs.io/en/latest/src/tutorial/memory_...
[+] [-] bnert|3 years ago|reply
[+] [-] simpleguitar|3 years ago|reply
[+] [-] janpaul123|3 years ago|reply
[+] [-] jeofken|3 years ago|reply
[+] [-] simjnd|3 years ago|reply
[1]: https://www.assemblyscript.org/
[+] [-] janpaul123|3 years ago|reply
[+] [-] orta|3 years ago|reply
https://github.com/orta/awesome-typescript-derived-languages...
[+] [-] wrnr|3 years ago|reply
[+] [-] janpaul123|3 years ago|reply
[+] [-] mattgreenrocks|3 years ago|reply
However, at this point, I'm mostly of the opinion that langs need some notion of mechanical sympathy if they want to be used for high-performance. JS engines probably have tens of millions of dollars of dev effort poured into them by now, yet they get soundly beaten by WASM, which is much younger. The conceptual foundation matters a lot.
That said, there's probably a nice space for an ergonomic, reasonably fast PL that sits between Rust and JS.
[+] [-] tomxor|3 years ago|reply
My initial instinct is that putting such considerations behind a preprocessor syntax has the disadvantage of making them more opaque and magic, and that I personally would continue to do such things more explicitly - which I realise is an ironic thing to say about an untyped, interpreted language, it's probably just the little pseudo "grey beard" on my shoulder which occasionally whispers silly things about "building their own computer out of rocks and twigs".
I guess the point is that there's a balance to the cost vs benefit: making it more convenient (what you call ergonomics), making it more accessible to those that were less likely to manually use that technique, and perhaps resulting in an average of more performant code; but at the cost of more obscurity, and more "magic", perhaps resulting in less educated programmers.
I'm probably overthinking it. Ultimately this argument even applies to for loops, but it's worth being conscious of that cost - it's likely a good idea.
[+] [-] transitivebs|3 years ago|reply
I was doing some high-level research on a related question the other day: https://transitivebullsh.it/webassembly-research-9e231b80e6d...
I know you've responded to a few people on here that Assemblyscript doesn't address the main issues you're talking about in the article, but honestly it does have a lot of things going for it.
I'd love to see a more direct breakdown of this space that explains why prior works like assemblyscript, nectarjs, walt, etc all fall short, and where an ideal solution would need to do better.
^^ this is the type of thing I'd love to chat about if you're interested btw
[+] [-] mc4ndr3|3 years ago|reply
I support efforts to tune existing tech stacks. But there is such a more direct path to performance, reliability, etc etc by dropping (alt)JS entirely now that anything can compile to WASM.
[+] [-] vanderZwan|3 years ago|reply
I suspect that if you could come up with an easy way to express a large tree really efficiently in a TypedArray in a relatively generic way, that you already will do better than most attempted solutions here.
[+] [-] pjmlp|3 years ago|reply
Maybe there are some ideas there.
[+] [-] novocantico|3 years ago|reply
[+] [-] Syzygies|3 years ago|reply
It doesn't look like you're making the same mistakes, but why trigger that prejudice?
[+] [-] chx|3 years ago|reply
type Vec2 = { x: number, y: number };
function avgLen(vecs: Vec2[]): number
versus:
struct Vec2 { x: f64, y: f64 }
fn avg_len(vecs: &[Vec2]) -> f64 {
There has been more than two decades between the release of Perl and Rust and you haven't learnt anything? Wow. Listen: code written in languages heavy with sigils and shorthand are hard to understand, read and most importantly: maintain. This adds a mental load which is -- as clearly visible from above -- is totally unnecessary. I have no idea why would anyone in 2010, several years after the famous "memory is the new disk, disk is the new tape" do this to save on characters in the source code. The teletype is a distant memory.
Thanks for letting me vent a little.
[+] [-] gpderetta|3 years ago|reply
Regarding shorthands, just number would be unacceptable for 99% of the work I do, and it would be something that specifies signedness, integer vs float and size. As a c++ programmer, having to write std::uint64_t gets old very quickly and I'm happy that rust uses a shorter name for these.
I don't think that using a shorthand for something very common is an hindrance to comprehension. Do you think one should write "have not" instead of "haven't" in English?
[+] [-] kouteiheika|3 years ago|reply
As someone who maintains a 145k line Rust codebase all on his own, I have to hard disagree on this one.
Once you get used to the syntax it just blends into the background and becomes a non-issue.
> This adds a mental load which is -- as clearly visible from above -- is totally unnecessary.
Totally unnecessary? Okay, so how would you do the following with the sigilless syntax (and disambiguate between them):
- Specify a unique reference? (`&T` is shared, `&mut T` is unique)
- Specify a lifetime? (`&'a T` is a reference to T with lifetime `a`)
- Specify a raw pointer? (`*const T` and `*mut T` are const/non-const raw pointers)
- Specify that the argument is moved into the function? (just the raw type `T`)
People have been trying to come up with a better syntax for Rust, and AFAIK so far nobody has been able to do it. There's always a tradeoff somewhere that ends up making matters worse than what we have now.
[+] [-] sondr3|3 years ago|reply
You could also write the function above as `fn avg_len(vecs: Vec<Vec2>) -> f64 {`, and I personally would write the TS one as `const avgLen = (vecs: Array<Vec2>): number`. However, there's a difference between the ownership of a `Vec` and `&[]`, something a language like TS doesn't need to worry about but Rust does.
[+] [-] zozbot234|3 years ago|reply
The more complex the underlying semantics you're working with, the more verbose and less symbol-driven your language should be. You can see this clearly with things like COBOL, which tried to integrate every feature that might ever be needed for business programming within its base language - a staggering amount of complexity. English-like syntax kept that workable.
[+] [-] mhaberl|3 years ago|reply
> This adds a mental load which is -- as clearly visible from above -- is totally unnecessary
For numbers in Typescript you basically have a number type (there is also that story about BigInt, but...) - this is as good as Rusts f64
In Rust you have: u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64
Sure, you could have some kind of alias number = f64 - but why? Rust and Typescript have different uses.
For the web, number (f64) is good enough. If you do system programming then you need some options.
[+] [-] triyambakam|3 years ago|reply
Nit pick: Every word in this sentence in the article is a link. This way of linking multiple related resources is so hard to keep track of and navigate. I need to hover over or click each word to discover the resource.
[+] [-] virchau13|3 years ago|reply
I just middle click to open each link in a new tab. I can read the new tab, close it, leave it open, whatever. The links I've already clicked will turn purple, meaning I can easily see which links have been visited already.
Is there something I'm missing?
[+] [-] pjmlp|3 years ago|reply
Note that Microsoft actually has a compiler from a Typescript subset into C++, as part of the MakeCode IoT education project.
[+] [-] janpaul123|3 years ago|reply
[+] [-] omegalulw|3 years ago|reply
[+] [-] cromwellian|3 years ago|reply
[+] [-] huydotnet|3 years ago|reply
[+] [-] quickthrower2|3 years ago|reply
Mnemonic: of = objects fail, in = index names
Yeah yeah objects can be arrays and iterable but you know what I mean. Best I could do! If you dont like just use the “in” mnemonic and Sherlock Holmes.
[+] [-] gigel82|3 years ago|reply
Worked ok for a while but broke down real quick once we brought in external npm dependencies.
Now we're just embedding V8 in our native apps and running the TS code through that. Not as interesting, but lets us import any random npm package (which is both good and bad).
[+] [-] dudus|3 years ago|reply
I think these cases will just never be good for WASM. DOM manipulation heavy Apps (which is a good chunk of JS code out there) may continue to be in JS and only the CPU intensive tasks would be computed in WASM.
Isn't that the best practice for WASM?
[+] [-] monssoen|3 years ago|reply
https://haxe.org/manual/types-abstract.html https://code.haxe.org/category/abstract-types/color.html
[+] [-] apatheticonion|3 years ago|reply
https://github.com/alshdavid/BorrowScript
[+] [-] rattray|3 years ago|reply
[+] [-] freeqaz|3 years ago|reply
Might be worth clarifying because I immediately got excited about a faster TypeScript compiler!
[+] [-] unknown|3 years ago|reply
[deleted]
[+] [-] lowbloodsugar|3 years ago|reply
[+] [-] langsoul-com|3 years ago|reply
[+] [-] nightski|3 years ago|reply
[+] [-] unknown|3 years ago|reply
[deleted]
[+] [-] chadcmulligan|3 years ago|reply