Skip to content

feat: upgrade tinygo to 0.40.1#35

Open
ic4l4s9c wants to merge 8 commits into
get-convex:mainfrom
ic4l4s9c:feat/upgrade-tinygo
Open

feat: upgrade tinygo to 0.40.1#35
ic4l4s9c wants to merge 8 commits into
get-convex:mainfrom
ic4l4s9c:feat/upgrade-tinygo

Conversation

@ic4l4s9c

@ic4l4s9c ic4l4s9c commented Mar 31, 2026

Copy link
Copy Markdown

Hey, not sure if this is needed or if there was a reason for keeping tinygo pinned to 0.33.0, but the go runtime's wasm_exec had drifted from upstream tinygo's implementation so I went through and aligned them.

Project now compiles with go@1.25.8 and tinygo@0.40.1, tests pass.

What changed:

  • Fixed finalizeRef — was wrong compared to tinygo's version (ref counting + id pool logic)
  • Fixed runtime.ticks and runtime.sleepTicks to match tinygo
  • Fixed syscall/js.stringVal (was missing the >>>= 0 unsigned coercion)
  • Fixed run, _resume, and proc_exit to match tinygo's exit/scheduler flow
  • Added missing WASI methods (clock_time_get, args_sizes_get, args_get) that newer tinygo imports — without these the implementation and tests break

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced WebAssembly process exit handling with proper exit code tracking
    • Fixed reference counting and resource cleanup
    • Improved runtime timing behavior and scheduler management
    • Corrected integer coercion in memory operations
    • Added WASI timing and argument support
  • Chores

    • Updated build tooling and compiler configurations

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes significantly rework the Go WASM runtime's exit handling mechanism, introduce new WASI function implementations, refactor timing and resource management, and update the TinyGo build toolchain version and target configuration.

Changes

Cohort / File(s) Summary
Go Runtime Architecture
src/component/lib/goRuntime.ts
Introduced wasmExit sentinel for exception-based exit signaling. Reworked proc_exit to record exit code and resolve pending exit promise. Restructured run() to detect and invoke exported _start with exception handling, then await exit promise. Enhanced WASI imports with clock_time_get, args_sizes_get, and args_get. Changed gojs.runtime.ticks to return BigInt nanoseconds and refactored sleepTicks to respect exit state. Implemented syscall/js.finalizeRef refcount decrementing and resource cleanup. Fixed syscall/js.stringVal unsigned 32-bit coercion.
Build Configuration
src/s2-bindings/build.py
Updated TinyGo version expectation from 0.33.0 to 0.40.1 and changed WASM OS target from wasi1p to wasip1.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant GoRuntime as Go Runtime (run)
    participant Start as _start Export
    participant ExitHandler as proc_exit Handler
    participant ExitPromise as Exit Promise
    
    Caller->>GoRuntime: run()
    alt _start export exists
        GoRuntime->>Start: try invoking _start()
        activate Start
        Start->>ExitHandler: calls wasi proc_exit(code)
        activate ExitHandler
        ExitHandler->>ExitHandler: mark exited=true, save exitCode
        ExitHandler->>ExitPromise: resolve with exit code
        deactivate ExitHandler
        ExitHandler-->>Start: throw wasmExit sentinel
        Start--xStart: exception propagates
        deactivate Start
        GoRuntime->>GoRuntime: catch wasmExit (swallow)
        GoRuntime->>ExitPromise: await pending exit
        ExitPromise-->>GoRuntime: resolved with exitCode
        GoRuntime-->>Caller: return exitCode
    else no _start export
        GoRuntime->>GoRuntime: call _initialize()
        GoRuntime-->>Caller: proceed normally
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 The WASM runtime hops with grace,
Exit promises find their place,
WASI calls now bloom anew,
Timings tick in nano-blue,
Resources cleanup, refcounts true! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: upgrade tinygo to 0.40.1' directly describes the main change—updating TinyGo version—but the PR also includes substantial runtime and WASI compatibility fixes that go beyond a simple version bump.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/component/lib/goRuntime.ts (1)

485-488: Redundant exit promise resolution.

Since proc_exit (lines 185-190) already calls this._resolveExitPromise() before throwing wasmExit, this check on lines 485-487 results in a duplicate call. While harmless (resolving a Promise multiple times is a no-op), it adds unnecessary code.

♻️ Suggested simplification
     try {
       this._inst.exports.resume();
     } catch (e) {
       if (e !== wasmExit) {
         throw e;
       }
     }
-    if (this.exited) {
-      this._resolveExitPromise();
-    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/component/lib/goRuntime.ts` around lines 485 - 488, Remove the redundant
resolution of the exit promise: delete the conditional block that checks
this.exited and calls this._resolveExitPromise() (the if (this.exited) {
this._resolveExitPromise(); } fragment) because proc_exit already invokes
this._resolveExitPromise() before throwing wasmExit; keep proc_exit and
_resolveExitPromise untouched so exit handling remains the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/component/lib/goRuntime.ts`:
- Around line 452-468: The run() logic can hang if this._inst.exports._start()
returns normally without throwing wasmExit (e.g., no proc_exit was called); add
a defensive fallback to resolve the exitPromise when _start() returns
synchronously. After the try/catch around this._inst.exports._start(), check
whether this._resolveExitPromise is still set and, if so, call it (or call
resolve directly) to ensure exitPromise is resolved and run() can return
this.exitCode; keep existing wasmExit handling and ensure you don't
double-resolve the promise.

---

Nitpick comments:
In `@src/component/lib/goRuntime.ts`:
- Around line 485-488: Remove the redundant resolution of the exit promise:
delete the conditional block that checks this.exited and calls
this._resolveExitPromise() (the if (this.exited) { this._resolveExitPromise(); }
fragment) because proc_exit already invokes this._resolveExitPromise() before
throwing wasmExit; keep proc_exit and _resolveExitPromise untouched so exit
handling remains the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5b6974e-e4bf-4d26-b30f-4269ca36aa9e

📥 Commits

Reviewing files that changed from the base of the PR and between d9ca9b2 and 6d6f013.

⛔ Files ignored due to path filters (1)
  • src/s2-bindings/go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • src/component/lib/goRuntime.ts
  • src/component/lib/s2Bindings.ts
  • src/component/lib/s2wasm.js
  • src/s2-bindings/build.py

Comment thread src/component/lib/goRuntime.ts
@ic4l4s9c ic4l4s9c force-pushed the feat/upgrade-tinygo branch from 6d6f013 to 5039570 Compare April 4, 2026 00:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/component/lib/goRuntime.ts`:
- Around line 185-189: proc_exit and _resume call the exit resolver
unconditionally but _resolveExitPromise is only set in the run() _start path, so
guard those calls for the _initialize path to avoid TypeError; update proc_exit
(and the similar handlers referenced at the other locations) to check that
this._resolveExitPromise is a function before invoking it (or provide a safe
no-op default during initialization), and ensure the same defensive check is
applied in _resume and any other places that call _resolveExitPromise so
reactor-side proc_exit completes the exit path rather than throwing.
- Around line 197-200: The current runtime.ticks implementation uses Date.now()
(wall-clock) which can jump; change it to use a monotonic base by computing a
timeOrigin = Date.now() - performance.now() once (e.g., at module init) and then
return ticks as BigInt(Math.floor((performance.now() + timeOrigin) * 1e6));
reference the runtime.ticks arrow function and ensure you use
globalThis.performance (with a Date.now() fallback if performance is not
available) so ticks matches TinyGo 0.40.1 monotonic semantics.
- Around line 133-145: Pointer parameters passed from WASM must be normalized to
unsigned 32-bit before being used as DataView/Uint8Array offsets to avoid
RangeError; update the WASI import functions (clock_time_get: normalize time_ptr
before calling mem().setBigUint64, args_sizes_get: normalize argc_ptr and
argv_buf_size_ptr before mem().setUint32, args_get: normalize any pointer args
it uses, fd_write: normalize iovs_ptr and nwritten_ptr, and random_get:
normalize bufPtr and any buffer offsets used in loadSlice) so they mirror the
unsigned coercion already applied in syscall/js.stringVal and ensure all
Uint8Array/DataView/BigInt writes use the unsigned pointer values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a61e68e8-07e7-462b-8ab2-f6e34aca5c62

📥 Commits

Reviewing files that changed from the base of the PR and between 6d6f013 and 5039570.

📒 Files selected for processing (1)
  • src/component/lib/goRuntime.ts

Comment on lines +133 to +145
clock_time_get: (_id: any, _precision: any, time_ptr: any) => {
const now = BigInt(Date.now() * 1e6);
mem().setBigUint64(time_ptr, now, true);
return 0;
},
args_sizes_get: (argc_ptr: any, argv_buf_size_ptr: any) => {
mem().setUint32(argc_ptr, 0, true);
mem().setUint32(argv_buf_size_ptr, 0, true);
return 0;
},
args_get: () => {
return 0;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the exact lines mentioned in the review
cat -n src/component/lib/goRuntime.ts | sed -n '130,150p'

Repository: get-convex/geospatial

Length of output: 918


🏁 Script executed:

# Check line 242 for the syscall/js.stringVal pattern mentioned
cat -n src/component/lib/goRuntime.ts | sed -n '235,250p'

Repository: get-convex/geospatial

Length of output: 733


🏁 Script executed:

# Search for other uses of >>>= 0 pattern in the file
rg ">>>= 0" src/component/lib/goRuntime.ts -B 2 -A 2

Repository: get-convex/geospatial

Length of output: 290


🏁 Script executed:

# Also check if there are other pointer-receiving WASI functions
rg "wasi_snapshot_preview1" src/component/lib/goRuntime.ts -A 3

Repository: get-convex/geospatial

Length of output: 267


🏁 Script executed:

# Let's look at more WASI functions to see if others have this pattern
cat -n src/component/lib/goRuntime.ts | sed -n '145,200p'

Repository: get-convex/geospatial

Length of output: 2310


🏁 Script executed:

# Search for all functions that receive pointers and use mem() operations
rg "mem\(\)\.(setBigUint64|setUint32|setUint8)" src/component/lib/goRuntime.ts -B 3 | head -50

Repository: get-convex/geospatial

Length of output: 1880


🏁 Script executed:

# Let's specifically check if fd_write and other WASI functions normalize pointers
cat -n src/component/lib/goRuntime.ts | sed -n '146,180p'

Repository: get-convex/geospatial

Length of output: 1572


🏁 Script executed:

# Let's check the loadSlice function to see if it normalizes pointers internally
rg -A 5 "const loadSlice" src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 413


🏁 Script executed:

# Check random_get more closely and the other syscall functions that take pointers
cat -n src/component/lib/goRuntime.ts | sed -n '190,195p'

Repository: get-convex/geospatial

Length of output: 270


🏁 Script executed:

# Let's check if other syscall/js functions normalize their pointers
rg '"syscall/js\.' src/component/lib/goRuntime.ts -A 3 | head -60

Repository: get-convex/geospatial

Length of output: 1842


🏁 Script executed:

# Let's check the loadString function implementation
rg -A 5 "const loadString" src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 225


🏁 Script executed:

# Let's also check if fd_write normalizes iovs_ptr at all
cat -n src/component/lib/goRuntime.ts | sed -n '147,160p'

Repository: get-convex/geospatial

Length of output: 691


🏁 Script executed:

# Check what mem() returns to understand how setBigUint64 and setUint32 work
rg -B 5 "const mem = " src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 185


🏁 Script executed:

# Let's check mem() implementation more fully
cat -n src/component/lib/goRuntime.ts | sed -n '70,85p'

Repository: get-convex/geospatial

Length of output: 556


🏁 Script executed:

# Let's check all pointer parameters used in mem() operations and arithmetic
rg "mem\(\)\.(get|set)" src/component/lib/goRuntime.ts | grep -E "ptr|Ptr" | head -20

Repository: get-convex/geospatial

Length of output: 448


🏁 Script executed:

# Check if there's any normalization pattern in other wasi functions
cat -n src/component/lib/goRuntime.ts | sed -n '147,195p'

Repository: get-convex/geospatial

Length of output: 2029


🏁 Script executed:

# Let me check the mem() function definition fully
cat -n src/component/lib/goRuntime.ts | sed -n '102,115p'

Repository: get-convex/geospatial

Length of output: 547


🏁 Script executed:

# Check if there are any actual issues with high-address pointers in the existing code
# by looking at what the memory buffer size typically is and if any code creates high pointers
rg "memory.buffer" src/component/lib/goRuntime.ts -B 3 -A 3

Repository: get-convex/geospatial

Length of output: 649


🏁 Script executed:

# Let me verify the WebAssembly i32 semantics by checking if there's documentation in the repo
find . -name "*.md" -o -name "*.txt" | xargs grep -l "i32\|pointer\|wasm" 2>/dev/null | head -5

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Let me check if there are any tests or examples that might use high-address pointers
find . -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "goRuntime\|wasi" 2>/dev/null | head -3

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Let me also verify the DataView API behavior with offset parameters in context
# Check if loadSlice is called with potentially high pointers
rg "loadSlice\(" src/component/lib/goRuntime.ts -B 2 -A 2

Repository: get-convex/geospatial

Length of output: 760


🏁 Script executed:

# Check if there are any patterns in how pointers are used before being passed to mem() operations
cat -n src/component/lib/goRuntime.ts | sed -n '247,270p'

Repository: get-convex/geospatial

Length of output: 1046


🏁 Script executed:

# Let me verify how mem() actually handles parameters by checking DataView behavior
# Check if there are any test files that might show expected behavior
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*test*.ts" \) -exec grep -l "clock_time_get\|args_sizes_get\|wasi" {} \;

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Let me search for any documentation or comments about WASM i32 conversion
rg "i32\|signed\|unsigned\|wasm" src/component/lib/goRuntime.ts | head -20

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Check the actual memory write pattern more carefully - look at how parameters are used
cat -n src/component/lib/goRuntime.ts | sed -n '100,125p'

Repository: get-convex/geospatial

Length of output: 935


🏁 Script executed:

# Let me check what happens with negative offsets in DataView/Uint8Array
# by looking at any error handling or comments
rg "RangeError\|byteOffset\|offset" src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Let me also check if there are similar normalization patterns elsewhere in the file
rg ">>>= 0" src/component/lib/goRuntime.ts -B 5 -A 5

Repository: get-convex/geospatial

Length of output: 372


🏁 Script executed:

# Let me look at the complete context of how fd_write handles pointers to be thorough
cat -n src/component/lib/goRuntime.ts | sed -n '147,180p'

Repository: get-convex/geospatial

Length of output: 1475


🏁 Script executed:

# Let me verify how DataView.setUint32 and setBigUint64 handle negative offsets
node <<'EOF'
const buffer = new ArrayBuffer(100);
const dv = new DataView(buffer);

// Try with negative offset
try {
  dv.setUint32(-1, 42, true);
  console.log("Negative offset accepted");
} catch (e) {
  console.log("Negative offset rejected:", e.message);
}

// Try with unsigned 32-bit high value passed as signed
const signedValue = -2147483648; // 0x80000000 as signed
const unsignedValue = signedValue >>> 0; // Convert to unsigned
console.log("Signed:", signedValue);
console.log("Unsigned:", unsignedValue);

// Test Uint8Array constructor with negative offset
try {
  const arr = new Uint8Array(buffer, -1, 10);
  console.log("Uint8Array with negative offset accepted");
} catch (e) {
  console.log("Uint8Array with negative offset rejected:", e.message);
}

// Test with converted unsigned offset
const arr = new Uint8Array(buffer, unsignedValue >>> 0, 1);
console.log("Uint8Array with converted offset succeeded");
EOF

Repository: get-convex/geospatial

Length of output: 917


🏁 Script executed:

# Let me also check if there are any runtime tests that exercise these WASI functions
find . -type f \( -name "*.ts" -o -name "*.js" \) -path "*/test*" -o -path "*/spec*" | xargs grep -l "goRuntime\|clock_time_get\|args_sizes_get" 2>/dev/null | head -5

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Let me look at all functions that receive pointer parameters to understand the full scope
rg "mem\(\)\.(get|set)(Uint|BigUint)" src/component/lib/goRuntime.ts -B 3 | grep -E "^\s+.*ptr.*=>|mem\(\)\.(get|set)" | head -30

Repository: get-convex/geospatial

Length of output: 1402


🏁 Script executed:

# Let me check if random_get and fd_write should also be normalized
# Look for any comments or issues about pointer handling
rg -i "pointer\|address\|offset\|signed\|unsigned" src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Check if there are any other pointer parameters that might need normalization
cat -n src/component/lib/goRuntime.ts | sed -n '191,195p'

Repository: get-convex/geospatial

Length of output: 252


🏁 Script executed:

# Let me also check if there's a pattern of when normalization is used
rg ">>>" src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 92


🏁 Script executed:

# Let me look at all the WASI and syscall/js functions more comprehensively
cat -n src/component/lib/goRuntime.ts | sed -n '131,200p'

Repository: get-convex/geospatial

Length of output: 2915


Add unsigned coercion for pointer parameters in WASI import functions.

WebAssembly exports i32 values to JavaScript as signed 32-bit numbers. When high-bit pointer values are used directly as offsets in DataView.setUint32(), mem().setBigUint64(), or the Uint8Array constructor (in loadSlice), they cause RangeError at runtime. This pattern is already applied in syscall/js.stringVal (line 242) and should be applied consistently across all WASI pointer parameters.

🔧 Suggested fix
         clock_time_get: (_id: any, _precision: any, time_ptr: any) => {
+          time_ptr >>>= 0;
           const now = BigInt(Date.now() * 1e6);
           mem().setBigUint64(time_ptr, now, true);
           return 0;
         },
         args_sizes_get: (argc_ptr: any, argv_buf_size_ptr: any) => {
+          argc_ptr >>>= 0;
+          argv_buf_size_ptr >>>= 0;
           mem().setUint32(argc_ptr, 0, true);
           mem().setUint32(argv_buf_size_ptr, 0, true);
           return 0;
         },

Also apply the same normalization to fd_write (both iovs_ptr and nwritten_ptr) and random_get (bufPtr).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
clock_time_get: (_id: any, _precision: any, time_ptr: any) => {
const now = BigInt(Date.now() * 1e6);
mem().setBigUint64(time_ptr, now, true);
return 0;
},
args_sizes_get: (argc_ptr: any, argv_buf_size_ptr: any) => {
mem().setUint32(argc_ptr, 0, true);
mem().setUint32(argv_buf_size_ptr, 0, true);
return 0;
},
args_get: () => {
return 0;
},
clock_time_get: (_id: any, _precision: any, time_ptr: any) => {
time_ptr >>>= 0;
const now = BigInt(Date.now() * 1e6);
mem().setBigUint64(time_ptr, now, true);
return 0;
},
args_sizes_get: (argc_ptr: any, argv_buf_size_ptr: any) => {
argc_ptr >>>= 0;
argv_buf_size_ptr >>>= 0;
mem().setUint32(argc_ptr, 0, true);
mem().setUint32(argv_buf_size_ptr, 0, true);
return 0;
},
args_get: () => {
return 0;
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/component/lib/goRuntime.ts` around lines 133 - 145, Pointer parameters
passed from WASM must be normalized to unsigned 32-bit before being used as
DataView/Uint8Array offsets to avoid RangeError; update the WASI import
functions (clock_time_get: normalize time_ptr before calling mem().setBigUint64,
args_sizes_get: normalize argc_ptr and argv_buf_size_ptr before mem().setUint32,
args_get: normalize any pointer args it uses, fd_write: normalize iovs_ptr and
nwritten_ptr, and random_get: normalize bufPtr and any buffer offsets used in
loadSlice) so they mirror the unsigned coercion already applied in
syscall/js.stringVal and ensure all Uint8Array/DataView/BigInt writes use the
unsigned pointer values.

Comment on lines 185 to +189
proc_exit: (code: any) => {
// Can't exit in a browser.
throw "trying to exit with code " + code;
this.exited = true;
this.exitCode = code;
this._resolveExitPromise();
throw wasmExit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard _resolveExitPromise for the _initialize() path.

run() only assigns this resolver inside the _start branch. In the _initialize branch it stays unset, but both proc_exit and _resume() call it unconditionally, so a later reactor-side proc_exit will throw a host TypeError instead of completing the exit path.

🔧 Suggested fix
@@
         proc_exit: (code: any) => {
           this.exited = true;
           this.exitCode = code;
-          this._resolveExitPromise();
+          if (this._resolveExitPromise) {
+            this._resolveExitPromise();
+          }
           throw wasmExit;
         },
@@
     this.exited = false; // whether the Go program has exited
     this.exitCode = 0;
+    this._resolveExitPromise = undefined;
@@
-    if (this.exited) {
+    if (this.exited && this._resolveExitPromise) {
       this._resolveExitPromise();
     }

Also applies to: 450-471, 485-487

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/component/lib/goRuntime.ts` around lines 185 - 189, proc_exit and _resume
call the exit resolver unconditionally but _resolveExitPromise is only set in
the run() _start path, so guard those calls for the _initialize path to avoid
TypeError; update proc_exit (and the similar handlers referenced at the other
locations) to check that this._resolveExitPromise is a function before invoking
it (or provide a safe no-op default during initialization), and ensure the same
defensive check is applied in _resume and any other places that call
_resolveExitPromise so reactor-side proc_exit completes the exit path rather
than throwing.

Comment on lines +197 to 200
// func ticks() int64
"runtime.ticks": () => {
return Date.now();
return BigInt(Date.now() * 1e6);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual file content around lines 197-200
cat -n src/component/lib/goRuntime.ts | sed -n '190,210p'

Repository: get-convex/geospatial

Length of output: 812


🏁 Script executed:

# Search for timeOrigin in the file
rg "timeOrigin" src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Get broader context around the runtime.ticks implementation
rg -A 5 -B 5 "runtime\.ticks" src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 328


🌐 Web query:

TinyGo 0.40.1 wasm_exec.js runtime.ticks performance.now timeOrigin

💡 Result:

In TinyGo 0.40.1, the wasm_exec.js file (located at $(tinygo env TINYGOROOT)/targets/wasm_exec.js) provides the JavaScript runtime environment for WASM/JS targets (built with GOOS=js GOARCH=wasm or -target=wasm). It defines the Go class and importObject.env with runtime functions imported by the WASM module, including gojs::runtime.ticks (seen in errors when misconfigured, e.g., using js target imports in wasip1). runtime.ticks likely returns high-resolution ticks (monotonic time) for the Go runtime time package, implemented using browser performance.now for precise timing (microsecond resolution vs Date.now's millisecond). timeOrigin is probably performance.timeOrigin, providing the absolute timestamp when performance.now started (Unix epoch ms). Recent changes (PR #4855, merged Apr 2025, included in 0.40.0+): wasm target updated to use int64 instead of float64 for timeUnit (nanoseconds per tick), improving precision/consistency. This affects time.Sleep, time.Now, timers (time.NewTimer implemented), avoiding float precision loss. No performance regressions noted in 0.40.1 changelog (bugfix release: nrf flash, rand fixes). Use matching wasm_exec.js version with compiler. For JS callbacks, go.importObject.env can override/add functions like 'add'.

Citations:


🏁 Script executed:

# Look at the full goRuntime.ts file to understand the class structure
wc -l src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 99


🏁 Script executed:

# Check the constructor and initialization area
head -100 src/component/lib/goRuntime.ts | tail -50

Repository: get-convex/geospatial

Length of output: 1298


🏁 Script executed:

# Look for where variables are declared in the gojs object
rg -B 20 "runtime\.ticks" src/component/lib/goRuntime.ts | head -30

Repository: get-convex/geospatial

Length of output: 679


🏁 Script executed:

# Check the constructor and instance variable declarations
sed -n '1,100p' src/component/lib/goRuntime.ts

Repository: get-convex/geospatial

Length of output: 2703


🏁 Script executed:

# Look for any timeOrigin setup or initialization
rg -i "timeorigin\|performance.now\|date.now" src/component/lib/goRuntime.ts -B 2 -A 2

Repository: get-convex/geospatial

Length of output: 47


🏁 Script executed:

# Check TinyGo 0.40.1 actual implementation directly
curl -s https://raw.githubusercontent.com/tinygo-org/tinygo/v0.40.1/targets/wasm_exec.js | grep -A 10 "runtime.ticks" | head -20

Repository: get-convex/geospatial

Length of output: 432


Use monotonic time for runtime.ticks to match TinyGo 0.40.1.

Line 199 diverges from TinyGo 0.40.1's implementation. Upstream uses performance.now() with a timeOrigin offset to provide monotonic time, while Date.now() is wall-clock time and can jump backwards when the system clock is adjusted. This breaks the monotonic timer semantics required by Go's runtime.

🔧 Suggested fix
      const loadSlice = (addr: any, len: any) => {
        return new Uint8Array(mem().buffer, addr, len);
      };
+
+    const timeOrigin = Date.now() - performance.now();

      this.importObject = {
      gojs: {
        // func ticks() int64
        "runtime.ticks": () => {
-         return BigInt(Date.now() * 1e6);
+         return BigInt((timeOrigin + performance.now()) * 1e6);
        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// func ticks() int64
"runtime.ticks": () => {
return Date.now();
return BigInt(Date.now() * 1e6);
},
const loadSlice = (addr: any, len: any) => {
return new Uint8Array(mem().buffer, addr, len);
};
const timeOrigin = Date.now() - performance.now();
this.importObject = {
gojs: {
// func ticks() int64
"runtime.ticks": () => {
return BigInt((timeOrigin + performance.now()) * 1e6);
},
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/component/lib/goRuntime.ts` around lines 197 - 200, The current
runtime.ticks implementation uses Date.now() (wall-clock) which can jump; change
it to use a monotonic base by computing a timeOrigin = Date.now() -
performance.now() once (e.g., at module init) and then return ticks as
BigInt(Math.floor((performance.now() + timeOrigin) * 1e6)); reference the
runtime.ticks arrow function and ensure you use globalThis.performance (with a
Date.now() fallback if performance is not available) so ticks matches TinyGo
0.40.1 monotonic semantics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant