feat: upgrade tinygo to 0.40.1#35
Conversation
📝 WalkthroughWalkthroughThe 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 callsthis._resolveExitPromise()before throwingwasmExit, 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
⛔ Files ignored due to path filters (1)
src/s2-bindings/go.sumis excluded by!**/*.sum
📒 Files selected for processing (4)
src/component/lib/goRuntime.tssrc/component/lib/s2Bindings.tssrc/component/lib/s2wasm.jssrc/s2-bindings/build.py
6d6f013 to
5039570
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
src/component/lib/goRuntime.ts
| 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; | ||
| }, |
There was a problem hiding this comment.
🧩 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 2Repository: 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 3Repository: 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 -50Repository: 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.tsRepository: 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 -60Repository: 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.tsRepository: 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.tsRepository: 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 -20Repository: 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 3Repository: 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 -5Repository: 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 -3Repository: 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 2Repository: 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 -20Repository: 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.tsRepository: 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 5Repository: 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");
EOFRepository: 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 -5Repository: 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 -30Repository: 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.tsRepository: 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.tsRepository: 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.
| 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.
| 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; |
There was a problem hiding this comment.
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.
| // func ticks() int64 | ||
| "runtime.ticks": () => { | ||
| return Date.now(); | ||
| return BigInt(Date.now() * 1e6); | ||
| }, |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.tsRepository: 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:
- 1: runtime: use int64 for
timeUniteverywhere tinygo-org/tinygo#4855 - 2: https://github.com/tinygo-org/tinygo/blob/release/CHANGELOG.md
- 3: https://tinygo.org/docs/guides/webassembly/wasm/
- 4:
GOOS=wasip1 GOARCH=wasmnot working,gojs::runtime.tickserror tinygo-org/tinygo#4682 - 5: https://github.com/tinygo-org/tinygo/releases/tag/v0.40.1
- 6: https://tinygo.org/docs/guides/webassembly/wasm
- 7: https://github.com/tinygo-org/tinygo/blob/release/main.go
- 8: Compiler feedback for 'target=wasm'+'gc=none' tinygo-org/tinygo#2495
🏁 Script executed:
# Look at the full goRuntime.ts file to understand the class structure
wc -l src/component/lib/goRuntime.tsRepository: get-convex/geospatial
Length of output: 99
🏁 Script executed:
# Check the constructor and initialization area
head -100 src/component/lib/goRuntime.ts | tail -50Repository: 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 -30Repository: get-convex/geospatial
Length of output: 679
🏁 Script executed:
# Check the constructor and instance variable declarations
sed -n '1,100p' src/component/lib/goRuntime.tsRepository: 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 2Repository: 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 -20Repository: 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.
| // 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.
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_exechad 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:
finalizeRef— was wrong compared to tinygo's version (ref counting + id pool logic)runtime.ticksandruntime.sleepTicksto match tinygosyscall/js.stringVal(was missing the>>>= 0unsigned coercion)run,_resume, andproc_exitto match tinygo's exit/scheduler flowclock_time_get,args_sizes_get,args_get) that newer tinygo imports — without these the implementation and tests breakBy 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
Chores