[HN Gopher] Porffor: A from-scratch experimental ahead-of-time J...
       ___________________________________________________________________
        
       Porffor: A from-scratch experimental ahead-of-time JS engine
        
       Author : bpierre
       Score  : 217 points
       Date   : 2024-07-30 18:55 UTC (4 hours ago)
        
 (HTM) web link (porffor.dev)
 (TXT) w3m dump (porffor.dev)
        
       | rubenfiszel wrote:
       | At windmill.dev, when users deploy their code, we use Bun build
       | (which is similar to esbuild) to bundle their scripts and all
       | their dependencies into a single js file to load which improve
       | cold start and memory usage. We store the bundle on s3 because of
       | the size of the bundles.
       | 
       | If we could bundle everything to native that would completely
       | change the game since as good as bun's cold start is, you can't
       | beat running straight native with a small binary.
        
       | solumos wrote:
       | Just out of curiosity, how does the performance (compilation +
       | runtime) compare to something like bun[0]?
       | 
       | [0] https://bun.sh/
        
       | ijustlovemath wrote:
       | I'd love to know if there's a way to compile NodeJS to native
       | libraries with this! I have a process [0], but it's a bit hacky
       | and error prone
       | 
       | [0] - https://github.com/ijustlovemath/jescx
        
       | awesomekling wrote:
       | Oliver (the main developer) just announced that they're going to
       | work full time on Porffor:
       | https://x.com/canadahonk/status/1818347311417938237
        
         | simlevesque wrote:
         | Financed by defunkt[1], GitHub cofounder and ex CEO, for an
         | undisclosed future project.
         | 
         | [1] https://news.ycombinator.com/user?id=defunkt
        
           | pityJuke wrote:
           | Also funded the Ladybird browser recently. Seems to like his
           | web.
        
       | obviouslynotme wrote:
       | I have thought about doing this and I just can't get around the
       | fact that you can't get much better performance in JS. The best
       | you could probably do is transpile the JS into V8 C++ calls.
       | 
       | The really cool optimizations come from compiling TypeScript, or
       | something close to it. You could use types to get enormous gains.
       | Anything without typing gets the default slow JS calls.
       | Interfaces can get reduced to vtables or maybe even straight
       | calls, possibly on structs instead of maps. You could have an Int
       | and Float type that degrade into Number that just sit inside
       | registers.
       | 
       | The main problem is that both TS and V8 are fast-moving, non-
       | standard targets. You could only really do such a project with a
       | big team. Maintaining compatibility would be a job by itself.
        
         | ch_sm wrote:
         | Somewhat related to this idea is AssemblyScript
         | https://www.assemblyscript.org
        
         | com2kid wrote:
         | Ecmascript 4 was an attempt to add better types to the
         | language, which sadly failed a long time ago.
         | 
         | It'd be nice of TS at least allowed for specifying types like
         | integer, allowing some of the newer TS aware runtimes could
         | take advantage of the additional info, even if the main TS->JS
         | compilation just treated `const val: int` the same as `const
         | val: number`.
         | 
         | I wonder if a syntax like                   const counter:
         | Number<int>
         | 
         | would be acceptable.
        
           | THBC wrote:
           | Number is not semantically compatible with raw 64-bit
           | integer, so you might as well wish for a native
           | const counter = UInt64(42);
           | 
           | The current state of the art is                   const
           | counter = BigInt.asUintN(64, 42);
        
           | obviouslynotme wrote:
           | Yeah, that is why I said TS (or something similar). TS made
           | some decisions that make sense at the time, but do not help
           | compilation. The complexity of its typing system is another
           | problem. I'm pretty sure that it is Turing-complete. That
           | doesn't remove feasibility, but it increases the complexity
           | of compiling it by a whole lot. When you add onto this the
           | fact that "the compiler is the spec," you really get bogged
           | down. It would be much easier to recognize a sensible subset
           | of TS. You could probably even have the type checker throw a
           | WTFisThisGuyDoing flag and just immediately downgrade it to
           | an any.
        
             | com2kid wrote:
             | > I'm pretty sure that it is Turing-complete.
             | 
             | Because JS code can arbitrarily modify a type, any language
             | trying to specify what the outputs of a function can be
             | also has to be Turing complete.
             | 
             | There are of course still plenty of types that TS doesn't
             | bother trying to model, but it does try to cover even funny
             | cases like field names going from kebab-case to camelCase.
        
           | the_imp wrote:
           | With Extractors [1] (currently at Stage 1), you could define
           | something like this to work:                   const Integer
           | = {           [Symbol.customMatcher]: (value) =>
           | [Number.parseInt(value)]         }              const
           | Integer(counter) = 42.56;         // counter === 42
           | 
           | [1] https://github.com/tc39/proposal-extractors
        
         | wk_end wrote:
         | At least without additional extensions, TypeScript would help
         | less than you think. It just wasn't designed for the job.
         | 
         | As a simple example - TypeScript doesn't distinguish between
         | integers and floats; they're all just numbers. So all array
         | accesses need casting. A TypeScript designed to aid static
         | compilation likely would have that distinction.
         | 
         | But the big elephant in the room is TypeScript's structural
         | subtyping. The nature of this makes it effectively impossible
         | for the compiler to statically determine the physical structure
         | of any non-primitive argument passed into a function. This
         | gives you worse-than-JIT performance on all field access, since
         | JITs can perform dynamic shape analysis.
        
           | obviouslynotme wrote:
           | Outside of really funky code, especially code originally
           | written in TS, you can assume the interface is the actual
           | underlying object. You could easily flag non-recognized-
           | member accesses to interfaces and then degrade them back to
           | object accesses.
        
             | wk_end wrote:
             | You're misunderstanding me, I think.
             | 
             | Suppose you have some interface with fields a and c. If
             | your function takes in an object with that interface and
             | operates on the c field, what you want is to be able to do
             | is compile that function to access c at "the address
             | pointed to by the pointer to the object, plus 8" (assuming
             | 64-bit fields). Your CPU supports such addressing directly.
             | 
             | Because of structural subtyping, you can't do that. It's
             | not unrecognized member. But your caller might pass in an
             | object with fields a, _b_ , and c. This is entirely
             | idiomatic. Now c is at offset 16, not 8. Because the
             | physical layout of the object is different, you no longer
             | have a statically known offset to the known field.
        
               | obviouslynotme wrote:
               | I would bet that, especially outside of library code,
               | 95+% of the typed objects are only interacted with using
               | a single interface. These could be turned into structs
               | with direct calls.
               | 
               | Outside of this, you can unify the types. You would take
               | every interface used to access the object and create a
               | new type that has all of the members of both. You can
               | then either create vtables or monomorphize where it is
               | used in calls.
               | 
               | At any point that analysis cannot determine the actual
               | underlying shape, you drop to the default any.
        
           | munificent wrote:
           | I think the even bigger elephant in the room is that
           | TypeScript's type system is unsound. You can have a function
           | whose parameter type is annotated to be String and there's
           | absolutely no guarantee that every call to that function will
           | pass it a string.
           | 
           | This isn't because of `any` either. The type system itself
           | deliberately has holes in it. So any language that uses
           | TypeScript type annotations to generate faster/smaller code
           | is opening itself to miscompiling code and segfaults, etc.
        
             | wk_end wrote:
             | So - I know this in theory, but avoided mentioning it
             | because I couldn't immediately think of any persuasive
             | examples (whereas subtype polymorphism is a core, widely
             | used, wholly unrestricted property of the language) that
             | didn't involve casts or any/unknown or other things that
             | people might make excuses for.
             | 
             | Do you have any examples off the top of your head?
        
               | neongreen wrote:
               | https://counterexamples.org/ is a good collection of
               | unsoundness examples in various languages.
               | 
               | For TypeScript, they list an example with `instanceof`:
               | 
               | https://counterexamples.org/polymorphic-union-
               | refinement.htm...
               | 
               | In the playground:
               | 
               | https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9m
               | ABA...
        
             | curtisblaine wrote:
             | It might be useful for an interpreter though. I believe
             | that in V8 you have this probabilistic mechanism in which
             | if the interpreter "learns" that an array contains e.g.
             | numbers consistently, it will optimize for numbers and
             | start accessing the array in a more performance way.
             | Typescript could be used to inform the interpreter even
             | before execution. (My supposition, I'm not an interpreter
             | expert)
        
           | pjmlp wrote:
           | Such Typescript already exists, Static Typescript,
           | 
           | https://makecode.com/language
           | 
           | Microsoft's AOT compiler for MakeCode, via C++.
        
           | cprecioso wrote:
           | > A TypeScript designed to aid static compilation likely
           | would have that distinction.
           | 
           | AssemblyScript (https://www.assemblyscript.org/) is a
           | TypeScript dialect with that distinction
        
         | refulgentis wrote:
         | You say you have "thought about doing this"..."[but] you can't
         | get much better performance", then describe the approach
         | requiring things that are described first-thing, above the
         | fold, on the site.
         | 
         | Did the site change? Or am I missing something? :)
        
       | syrusakbary wrote:
       | It's awesome to see how more JS runtimes try to approach Wasm.
       | This project reminds me to Static Hermes (the JS engine from
       | Facebook to improve the speed of React Native projects on iOS and
       | Android).
       | 
       | I've spent a bit of time trying to review each, so hopefully this
       | analysis will be useful for some readers. What are the main
       | commonalities and differences between Static Hermes and Porffor?
       | * They both aim for JS test262 conformance [1]       * Porffor
       | supports both Native and Wasm outputs while Static Hermes is
       | mainly focused on Native outputs for now       * Porffor is self-
       | hosted (Porffor is written in pure JS and can compile itself),
       | while Static Hermes relies on LLVM       * Porffor currently
       | doesn't support async/promise/await while Static Hermes does
       | (with some limitations)       * Static Hermes is written in C++
       | while Porffor is mainly JS       * They both support TypeScript
       | (although Static Hermes does it through transpiling the TS AST to
       | Flow, while Porffor supports it natively)       * Static Hermes
       | has a fallback interpreter (to support `eval` and other hard-to-
       | compile JS scenarios), while Porffor only supports AOT compiling
       | (although, as I commented in other thread here, it maybe be
       | possible to support `eval` in Porffor as well)
       | 
       | In general, I'm excited to see if this project can gain some
       | traction so we can speed-up Javascript engines one the Edge!
       | Context: I'm Syrus, from Wasmer [3]
       | 
       | [1] https://github.com/facebook/hermes/discussions/1137
       | 
       | [2] https://github.com/tc39/test262
       | 
       | [3] https://wasmer.io
        
         | jonathanyc wrote:
         | Just wanted to say I really appreciated the high-quality
         | comparison. How something compares to existing work is my #1
         | question whenever I read an announcement like this.
        
           | syrusakbary wrote:
           | Thanks!
        
       | saagarjha wrote:
       | What happens when someone calls eval?
        
         | syrusakbary wrote:
         | Since Porffor can compile itself (you can run the compiler
         | inside of Porffor), any calls to eval could be compiled to Wasm
         | (via executing the Porffor compiler in Porffor JS engine) and
         | executed performantly on the same JS context *
         | 
         | *or at least, in theory
        
           | tasty_freeze wrote:
           | I haven't used it, but reading their landing page, Porffor
           | says their runtime is vastly smaller because it is AOT. If
           | the compiler had to be bundled with the executable, then the
           | size of the executable would grow much larger.
        
         | david2ndaccount wrote:
         | With a string literal it works, with a dynamic string it just
         | gives an undefined reference error to eval.
        
         | canadahonk wrote:
         | For now, unless it is given a literal string (eg `eval('42')`)
         | eval just won't work.
        
       | THBC wrote:
       | This seems like an opaque supply chain attack waiting to happen.
        
       | nick_g wrote:
       | I'm a bit suspicious of the versioning scheme described here[0]
       | 
       | If some change were required which introduced a regression on
       | some Test262 tests, it could cause the version number to regress
       | as well. This means Porffor cannot have both a version number
       | which increases monotonically and the ability to introduce
       | necessary changes which cause Test262 regressions
       | 
       | [0] https://github.com/CanadaHonk/porffor?tab=readme-ov-
       | file#ver...
        
         | derdi wrote:
         | Presumably the idea is that any work that causes Test262
         | regressions is temporary, takes place in a separate branch, and
         | is only merged to main once the branch also contains all the
         | necessary fixes to make the regressions go away again. A new
         | version number would only be used once that merge happens.
        
       | rvnx wrote:
       | Seems like the same idea that Facebook had with PHP which was to
       | transpile PHP to C.
       | 
       | It was called hiphop-php, then they eventually gave up, before
       | creating hhvm on a complete new concept.
        
         | pjmlp wrote:
         | Historically the sequence is a bit different.
         | 
         | After HHVM proved that its JIT compilation engine was faster
         | than their HipHop AOT attempt, did they decided to focus only
         | on HHVM going forward.
        
       | Borkdude wrote:
       | I got "TodoError: no generation for ImportDeclaration!" for this
       | script:
       | 
       | import * as squint_core from 'squint-cljs/core.js';
       | console.log("hello");
        
       | mproud wrote:
       | "Purple" in Welsh
        
         | zem wrote:
         | with etymology from the greek for purple; the english
         | "porphyry" (a purple mineral) is probably the commonest word
         | with the same root.
        
       ___________________________________________________________________
       (page generated 2024-07-30 23:00 UTC)