Skip to main content
Porffor intentionally uses only widely-implemented WebAssembly proposals to maximize compatibility across runtimes. Some proposals are required, while others are optional.
From README.md:129-136: Porffor does not use cutting-edge proposals like GC (Garbage Collection) to ensure it can run in as many places as possible.

Required Proposals

These proposals are mandatory for running Porffor-compiled WebAssembly:

Multi-value

Required
Allows WebAssembly functions to return multiple values.

Why It’s Needed

Porffor represents every value as a pair: (value, type). Functions must return both:
// JavaScript function
function add(a, b) {
  return a + b;
}

// Compiled Wasm signature (simplified)
// Returns: (f64 result, i32 type)
(func $add (param $a f64) (param $a_type i32) 
           (param $b f64) (param $b_type i32)
           (result f64) (result i32)
  ;; ... implementation ...
  ;; Push result value
  ;; Push result type
)
Without multi-value, this would be impossible—functions could only return a single value.

Browser Support

Chrome 85+ (2020)
Firefox 78+ (2020)
Safari 15+ (2021)
Node.js 15+ (2020)

Technical Details

From CONTRIBUTING.md:267-270:
;; Declare return type as multiple values
returns i32 i32

;; Function can now return two i32 values
i32.const 42
i32.const 1  ;; TYPES.number
return

Non-trapping Float-to-Int Conversions

Required
Provides safe float-to-integer conversion that never traps (throws).

The Problem

Standard Wasm float-to-int instructions trap on overflow:
;; Standard (trapping) conversion
f64.const 1e100  ;; Huge number
i32.trunc_f64_s  ;; ❌ TRAPS! Number too large for i32

The Solution

Saturating conversions clamp instead of trapping:
;; Non-trapping (saturating) conversion  
f64.const 1e100
i32.trunc_sat_f64_s  ;; ✅ Returns i32::MAX (2147483647)

f64.const -1e100  
i32.trunc_sat_f64_s  ;; ✅ Returns i32::MIN (-2147483648)

f64.const nan
i32.trunc_sat_f64_s  ;; ✅ Returns 0

Usage in Porffor

From compiler/wasmSpec.js:228-231:
export const Opcodes = {
  // ...
  i32_trunc_sat_f64_s: [ 0xfc, 0x02 ],  // Signed
  i32_trunc_sat_f64_u: [ 0xfc, 0x03 ],  // Unsigned
  i64_trunc_sat_f64_s: [ 0xfc, 0x06 ],
  i64_trunc_sat_f64_u: [ 0xfc, 0x07 ],
};
Used extensively in compiler/codegen.js for type conversions:
// From codegen.js:291
Opcodes.i32_trunc_sat_f64_s,

// From codegen.js:7559-7560  
Opcodes.i32_to = valtypeBinary === Valtype.i32 ? [] : Opcodes.i32_trunc_sat_f64_s;
Opcodes.i32_to_u = valtypeBinary === Valtype.i32 ? [] : Opcodes.i32_trunc_sat_f64_u;

Browser Support

Chrome 75+ (2019)
Firefox 65+ (2019)
Safari 15+ (2021)
Node.js 12+ (2019)

Optional Proposals

These proposals enhance functionality but aren’t strictly required:

Bulk Memory Operations

Optional - Graceful degradation possible
Provides efficient memory operations for copying and filling.

Instructions

From compiler/wasmSpec.js:233-235:
export const Opcodes = {
  memory_init: [ 0xfc, 0x08 ],  // Copy from data segment
  data_drop: [ 0xfc, 0x09 ],    // Drop data segment
  memory_copy: [ 0xfc, 0x0a ],  // Copy memory region
};

memory.copy Example

From compiler/builtins.js:1027:
;; Copy bytes from source to destination
memory.copy
  (i32.const $dest)   ;; Destination pointer
  (i32.const $src)    ;; Source pointer
  (i32.const $size)   ;; Number of bytes
Without bulk memory, Porffor falls back to byte-by-byte copying:
memory.copy
  local.get $dest
  local.get $src  
  i32.const 1000  ;; Copy 1000 bytes instantly
Bulk memory operations are typically 10-100× faster than loops for large copies.

Browser Support

Chrome 75+ (2019)
Firefox 79+ (2020)
Safari 15+ (2021)
Node.js 12.5+ (2019)

Exception Handling

Optional - Only for errors
Provides structured exception handling with try/catch.

Opcodes

From compiler/wasmSpec.js:69-74:
export const Opcodes = {
  try: 0x06,
  catch: 0x07,
  catch_all: 0x19,
  delegate: 0x18,
  throw: 0x08,
  rethrow: 0x09,
};

Exception Modes

Porffor supports two exception modes:
porf script.js
# Uses stack-based exceptions
Exceptions carry the full error object on the stack:
throw new TypeError("Invalid argument");
// Stack contains: (error_object, error_type)
From compiler/codegen.js:5470:
params: [ valtypeBinary, Valtype.i32 ]  // (value, type)

Usage Example

try {
  throw new Error("Something went wrong");
} catch (e) {
  console.log(e.message);
}
Compiles to:
(try
  ;; Code that might throw
  (throw $error_tag (i32.const 0))  ;; Exception ID 0
(catch $error_tag
  ;; Handle exception
  ;; Stack has exception data
)
)

Browser Support

Limited support - newest proposal used by Porffor
Chrome 95+ (2021)
Firefox 100+ (2022)
Node.js 17+ (2021)
Without exception handling:
  • Uncaught errors will crash the Wasm instance
  • No stack traces for errors
  • try/catch blocks won’t work

Tail Calls

Opt-in - Disabled by default
Enables tail call optimization to prevent stack overflow in recursive functions.

Enabling Tail Calls

porf --tail-call script.js
From compiler/opt.js:11: The tail call proposal is not widely implemented! Only enable if you know your target runtime supports it.

How It Works

Without tail calls:
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc);  // ❌ Grows stack
}

factorial(10000);  // Stack overflow!
With tail calls:
function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc);  // ✅ Reuses stack frame
}

factorial(10000);  // Works fine!
The compiler transforms the call into a return_call instruction:
;; Without tail calls
(call $factorial)    ;; Pushes new stack frame
(return)

;; With tail calls (opt.js:204-214)
(return_call $factorial)  ;; Reuses current stack frame

Browser Support

Very limited support
Chrome 112+ (2023) - Behind flag
Node.js 20+ (2023) - Behind flag

Performance Impact

From compiler/opt.js:211-214:
if (Prefs.optLog) log('opt', `tail called return, call`);
With tail calls enabled, the optimizer can convert:
  • Regular call + returnreturn_call (saves stack)
  • Regular call_indirect + returnreturn_call_indirect

Proposals NOT Used

Porffor intentionally avoids newer proposals for compatibility:

GC Proposal

Not used - Would enable automatic garbage collection but has limited runtime support.

Threads

Not used - SharedArrayBuffer support exists but no thread management.

SIMD (partial)

Minimal use - Some SIMD operations defined but not heavily used.

Reference Types

Limited use - funcref and externref defined but not core to compilation.

Checking Runtime Support

You can check if a runtime supports required proposals:
// Check for multi-value support
const multiValue = WebAssembly.validate(
  new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, /* ... */])
);

// Check for non-trapping conversions
const nonTrapping = typeof WebAssembly.Module.imports !== 'undefined';

// Check for bulk memory
const bulkMemory = WebAssembly.validate(
  new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, /* ... */])
);
Or check Node.js version:
node --version
# v15.0.0 or higher = all required proposals ✅
# v12.0.0 - v14.x = missing multi-value ❌

Summary Table

ProposalStatusSinceUsed ForFallback
Multi-valueRequired2020Function returns (value, type)None
Non-trapping f2iRequired2019Safe type conversionsNone
Bulk memoryOptional2020Fast memory copy/fillByte loops
Exception handlingOptional2021try/catch blocksCrash on error
Tail callsOpt-in2023Recursive optimizationStack overflow

Minimum Runtime Versions

To run Porffor-compiled Wasm:

Chrome

Version 85+ (August 2020)All required proposals supported.

Firefox

Version 79+ (July 2020)All required + bulk memory.

Safari

Version 15+ (September 2021)Required proposals only. No exception handling yet.

Node.js

Version 15+ (October 2020)Recommended: v20+ for best support.

Next Steps

Memory Model

See how bulk memory operations work

AOT Compilation

Understand the compilation process

Debugging

Debug compiled code

Compiler Options

Configure exception handling and other features

Build docs developers (and LLMs) love