GoScript translates Go code to TypeScript. This document outlines the design principles, translation strategies, and known divergences from Go semantics. The goal is to produce idiomatic, readable, and maintainable TypeScript code that preserves the core logic and type safety of the original Go code where possible.
- AST Mapping: Aim for a close mapping between the Go AST (
go/ast) and the TypeScript AST, simplifying the compiler logic. - Type Preservation: Preserve Go's static typing as much as possible using TypeScript's type system.
- Value Semantics: Emulate Go's value semantics for basic types and structs using copying where necessary. Pointers are used to emulate reference semantics when Go uses pointers. See VarRefes and Pointers.
- Idiomatic TypeScript: Generate TypeScript code that feels natural to TypeScript developers, even if it means minor divergences from exact Go runtime behavior (e.g.,
for rangeloop variable scoping). - Readability: Prioritize clear and understandable generated code.
- Go packages are translated into TypeScript modules (ES modules).
- Each Go file within a package is typically translated into a corresponding TypeScript file.
- The
mainpackage andmainfunction have special handling to produce an executable entry point (details TBD). - Imports are translated to TypeScript
importstatements. The GoScript runtime is imported as@goscript/builtin.
- Basic Types: Go basic types (
int,string,bool,float64, etc.) are mapped to corresponding TypeScript types or custom types provided by the runtime (@goscript/builtin).int,uint,int64, etc. ->$.int(currently represented asnumberorbigintdepending on configuration/needs, potentially using a custom class for overflow checks).float64,float32->numberstring->stringbool->booleanrune->$.rune(likelynumber)byte->$.byte(likelynumber)error->$.error(interface, typicallyError | null)
- Composite Types:
- Structs: Translated to TypeScript classes. Fields are mapped to class properties. Value semantics are maintained by cloning instances on assignment or passing as arguments, unless pointers are used. See
DESIGN_STRUCTS.md(TODO: Create this file). - Arrays: Translated to TypeScript arrays (
T[]). Go's fixed-size nature might require runtime checks or specific handling if strictness is needed. - Slices: Translated to a custom
$.Slice<T>type/class from the runtime to handle Go's slice semantics (length, capacity, underlying array). - Maps: Translated to TypeScript
Map<K, V>. - Channels: Translated using helper classes/functions from the runtime (
$.Chan<T>) potentially leveraging async iterators or libraries likecsp-ts. SeeDESIGN_CONCURRENCY.md(TODO: Create this file). - Interfaces: Translated to TypeScript interfaces. Type assertions and type switches require runtime type information or helper functions. See
DESIGN_INTERFACES.md(TODO: Create this file). - Pointers: Translated using a
$.VarRef<T>wrapper type from the runtime. See VarRefes and Pointers.
- Structs: Translated to TypeScript classes. Fields are mapped to class properties. Value semantics are maintained by cloning instances on assignment or passing as arguments, unless pointers are used. See
- Function Types: Translated to TypeScript function types.
vardeclarations are translated toletorvar(TBD, likelylet). Type inference is used where possible. Zero values are assigned explicitly.constdeclarations are translated toconst.- Short variable declarations (
:=) are translated toletwith type inference.
if/else: Translated directly to TypeScriptif/else. Scoped simple statements (if x := foo(); x > 0) are handled by declaring the variable before theif.switch: Translated to TypeScriptswitch. Type switches require special handling using runtime type information.for:- Standard
forloops (for init; cond; post) are translated directly to TypeScriptforloops. for condloops are translated to TypeScriptwhile (cond).for {}loops are translated towhile (true).for range: Translated to TypeScriptfor...ofloops.- Go Behavior: Go's
for rangereuses the same loop variable(s) for each iteration. If these variables are captured by a closure (e.g., inside a goroutine ordefer), the closure will reference the final value of the variable after the loop finishes, which is a common source of bugs. - TypeScript
for...ofBehavior: When usingletorconstwithfor...of, TypeScript (and modern JavaScript) creates a new binding for the loop variable(s) in each iteration. This avoids the closure capture pitfall common in Go. - GoScript Translation: GoScript translates Go
for rangeloops into TypeScriptfor...ofloops usingletfor the loop variables.// Go: for i, v := range mySlice { ... } // TS: for (let [i, v] of $.range(mySlice)) { ... } // or similar helper // Go: for k := range myMap { ... } // TS: for (let k of myMap.keys()) { ... } // or $.rangeMapKeys(myMap) // Go: for _, v := range myString { ... } // iterating runes // TS: for (let v of $.rangeString(myString)) { ... }
- Divergence: This translation intentionally diverges from Go's exact variable reuse semantic. By creating a new binding per iteration (
let), the generated TypeScript code avoids the common Go pitfall where closures accidentally capture the final loop variable value. This results in code that is often more correct and aligns better with JavaScript/TypeScript developers' expectations. The compliance testcompliance/tests/for_range/demonstrates this behavior.
- Go Behavior: Go's
- Standard
defer: Translated using atry...finallyblock and a helper stack/array managed by the runtime ($.defer). SeeDESIGN_DEFER.md(TODO: Create this file).go: Translated using asynchronous functions (async/await) and potentially runtime helpers ($.go). SeeDESIGN_CONCURRENCY.md(TODO: Create this file).select: Translated using runtime helpers, likely involvingPromise.raceor similar mechanisms. SeeDESIGN_CONCURRENCY.md(TODO: Create this file).
- Go functions are translated to TypeScript functions.
- Go methods are translated to TypeScript class methods.
- Multiple return values are handled by returning tuples (arrays) or objects. The call site uses destructuring assignment.
- Variadic functions (
...T) are translated using rest parameters (...args: T[]).
- Most operators map directly (
+,-,*,/,%,==,!=,<,>,<=,>=,&&,||,!). - Bitwise operators (
&,|,^,&^,<<,>>) require runtime helper functions ($.bitAnd(), etc.) especially for integer types beyond JavaScript's standard number representation or for correct handling of signed vs unsigned shifts. - Pointer operations (
*,&) are handled via the$.VarRef<T>type and its methods/properties (e.g.,*pbecomesp.value,&xbecomes$.varRef(x)or handled implicitly via assignment).
See DESIGN_CONCURRENCY.md (TODO: Create this file). Go's goroutines and channels are mapped to TypeScript's async/await and custom runtime implementations for channels.
Go's explicit error return values are maintained. Functions returning an error typically have a TypeScript signature like (...args: TArgs) => [TResult, $.error] or (...args: TArgs) => $.error. Call sites check the error value.
len(): Mapped to.lengthfor arrays/strings/slices,.sizefor maps, or runtime helpers.cap(): Mapped to runtime helpers for slices/channels.append(): Mapped to a runtime helper function$.append().make(): Mapped to runtime helper functions ($.makeSlice(),$.makeMap(),$.makeChan()).new(): Mapped to$.varRef(new T())or similar, returning a pointer ($.VarRef<T>) to a zero value.copy(): Mapped to a runtime helper function$.copy().delete(): Mapped tomap.delete().panic()/recover(): Mapped to throwing exceptions andtry...catchwith runtime helpers ($.panic(),$.recover()). SeeDESIGN_PANIC_RECOVER.md(TODO: Create this file).print()/println(): Mapped toconsole.logor similar.
See design/VAR_REFS.md. Go pointers are represented using a $.VarRef<T> wrapper type provided by the runtime. This allows emulating pointer semantics (shared reference, ability to modify the original value indirectly) in TypeScript.
- Taking the address (
&x): Often implicit when assigning to a variable expecting a$.VarRef<T>, or explicitly$.varRef(x). - Dereferencing (
*p): Accessing thep.valueproperty. - Pointer assignment (
p = q): Assigns the$.VarRefreference. - Assigning to dereferenced pointer (
*p = y): Modifyingp.value = y.
Value types (structs, basic types) are copied on assignment unless they are variable references.
The runtime provides:
- Helper types (
$.int,$.error,$.Slice,$.Chan,$.VarRef, etc.). - Helper functions (
$.makeSlice,$.append,$.copy,$.panic,$.recover,$.go,$.defer, bitwise operations, etc.). - Runtime type information utilities (for type assertions/switches).
- Integer Overflow: Standard TypeScript numbers do not overflow like Go's fixed-size integers. Using
BigIntor custom classes via$.intcan mitigate this but adds complexity. Current implementation may use standard numbers with potential divergence on overflow. - Floating Point Precision: Differences may exist between Go's
float64/float32and TypeScript'snumber(IEEE 754 64-bit float). for rangeVariable Scoping: Go reuses loop variables, while GoScript's translation tofor...ofwithletcreates new bindings per iteration to avoid common closure capture bugs (see Control Flow).- Concurrency Model:
async/awaitprovides cooperative multitasking, differing from Go's preemptive goroutine scheduling. Subtle timing and fairness differences may exist. - Panic/Recover vs. Exceptions: While mapped, the exact stack unwinding and recovery mechanisms might differ subtly from Go's
panic/recover. - Zero Values: Explicit assignment is used, but subtle differences in initialization order compared to Go's implicit zeroing might occur in complex scenarios (e.g., during package initialization).
- Refine integer type handling (
BigIntvs. custom class vs. number). - Detailed design docs for Structs, Interfaces, Concurrency, Defer, Panic/Recover.
- Build System/Package Management integration.
- Source Maps for debugging.
- Implement standard library including "runtime" functions
This is the typical package structure of the output TypeScript import path:
@goscript/ # Typical Go workspace, all packages live here. Includes the '@goscript/builtin' alias for the runtime.
# Compiled Go packages go here (e.g., @goscript/my/package)
This section highlights key areas where GoScript's generated TypeScript behavior differs from standard Go, primarily due to the challenges of mapping Go's memory model and semantics directly to JavaScript/TypeScript.
• Value Receiver Method Semantics:
- In Go, methods with value receivers (
func (s MyStruct) Method()) operate on a copy of the receiver struct. - In GoScript, both value and pointer receiver methods are translated to methods on the TypeScript class. Calls to value-receiver methods on a TypeScript instance modify the original object referenced by
this, not a copy. This differs from Go's copy-on-call semantics for value receivers.
After reviewing the code and tests, some important implementation considerations include:
-
Pointer Comparison Implementation:
- Ensure pointer comparisons use appropriate TypeScript equality semantics.
- Pointer comparison operators (
==,!=,==nil) must be correctly translated to their TypeScript equivalents.
-
Pointer Representation, Variable References & Addressability:
- Variable References: To handle Go's pointers and addressability within JavaScript's reference model, GoScript employs a "variable reference" strategy. When the address of a variable (
&v) is taken anywhere in the code, that variable is declared using the$.VarRef<T>type from the runtime (@goscript/builtin). This variable reference holds the actual value and allows multiple pointers to reference and modify the same underlying value.// Go var x int = 10 p := &x // Address of x is taken, so x must be a variable reference
// TypeScript import * as $ from "@goscript/builtin" let x: $.VarRef<number> = $.varRef(10) // x is a variable reference let p: $.VarRef<number> | null = x // p points to the variable reference x
- Addressability: Only variables that have been made variable references (because their address was taken) are addressable.
- Pointer Types: Go pointer types are mapped to potentially nullable
VarReftypes in TypeScript. See the "Type Mapping" section for details. - Multi-level Pointers: A variable (which can itself be a pointer) becomes a variable reference if its own address is taken.
This translates to:
// Go (Example from compliance/tests/varRef/varRef.go) var x int = 10 // x is a variable reference because p1 takes its address var p1 *int = &x // p1 is a variable reference because p2 takes its address var p2 **int = &p1 // p2 is a variable reference because p3 takes its address var p3 ***int = &p2 // p3 is NOT a variable reference because its address is not taken
Dereferencing// TypeScript import * as $ from "@goscript/builtin" let x: $.VarRef<number> = $.varRef(10); let p1: $.VarRef<$.VarRef<number> | null> = $.varRef(x); // p1's variable reference holds a reference to x's variable reference let p2: $.VarRef<$.VarRef<$.VarRef<number> | null> | null> = $.varRef(p1); // p2's variable reference holds a reference to p1's variable reference let p3: $.VarRef<$.VarRef<$.VarRef<number> | null> | null> | null = p2; // p3 is not a variable reference; it directly holds the reference to p2's variable reference
***p3then becomesp3!.value!.value!.value.
- Variable References: To handle Go's pointers and addressability within JavaScript's reference model, GoScript employs a "variable reference" strategy. When the address of a variable (
-
Value Semantics for Structs:
- Go's value semantics for structs (copying on assignment) need to be properly implemented in TypeScript.
- Method calls on value receivers versus pointer receivers need to behave consistently with Go semantics.
- Exported Identifiers: Go identifiers (functions, types, variables, struct fields, interface methods) that are exported (start with an uppercase letter) retain their original PascalCase naming in the generated TypeScript code. For example,
MyFunctionin Go becomesexport function MyFunction(...)in TypeScript, andMyStruct.MyFieldbecomesMyStruct.MyField. - Unexported Identifiers: Go identifiers that are unexported (start with a lowercase letter) retain their original camelCase naming in the generated TypeScript. If they are fields of an exported struct, they become public fields in the TypeScript class.
- Keywords: Go keywords are generally not an issue, but care must be taken if a Go identifier clashes with a TypeScript keyword.
-
Built-in Types: Go built-in types are mapped to corresponding TypeScript types (e.g.,
string->string,int->number,bool->boolean,float64->number). If the address of a variable with a built-in type is taken, it's wrapped in$.VarRef<T>(e.g.,$.VarRef<number>). -
String and Rune Conversions: Go's
runetype (an alias forint32representing a Unicode code point) and its interaction withstringare handled as follows:runeType: Translated to TypeScriptnumber.string(rune)Conversion: The Go conversion from aruneto astringcontaining that single character is translated usingString.fromCharCode():becomes:var r rune = 'A' // Unicode point 65 s := string(r)
let r: number = 65 let s = String.fromCharCode(r) // s becomes "A"
[]rune(string)Conversion: Converting astringto a slice ofrunes requires a runtime helper to correctly handle multi-byte Unicode characters:becomes (conceptual translation using a runtime helper):runes := []rune("Go€")
(This helper was also seen in theimport * as $ from "@goscript/builtin" let runes = $.stringToRunes("Go€") // runes becomes [71, 111, 8364]
for rangeover strings translation).string([]rune)Conversion: Converting a slice ofrunes back to astringalso requires a runtime helper:becomes (conceptual translation using a runtime helper):s := string([]rune{71, 111, 8364})
import * as $ from "@goscript/builtin" let s = $.runesToString([71, 111, 8364]) // s becomes "Go€"
Note: Direct conversion between
stringand[]bytewould involve different runtime helpers focusing on byte representations. -
Structs: Converted to TypeScript
classes. Both exported and unexported Go fields becomepublicmembers in the TypeScript class. Aclone()method is added to support Go's value semantics on assignment/read. Thisclone()method performs a deep copy of the struct's fields, including recursively cloning any nested struct fields, to ensure true value semantics.- Constructor Initialization: The generated class constructor accepts an optional
initobject. Fields are initialized using this object. Crucially, for fields that are themselves struct values (not pointers):- If the corresponding value in
initis provided, it is cloned using its.clone()method before assignment to maintain Go's value semantics (e.g.,this._fields.InnerStruct = $.varRef(init?.InnerStruct?.clone() ?? new InnerStruct())). - If the corresponding value in
initis nullish (nullorundefined), the field is initialized with a new instance of the struct's zero value (new FieldType()) to maintain parity with Go's non-nullable struct semantics. - Pointer fields are initialized to
nullif theinitvalue is nullish (no cloning needed).
// Example generated constructor logic for a field 'Inner' of type 'InnerStruct' public Inner: InnerStruct // ... other fields ... constructor(init?: { Inner?: InnerStruct /* ... other fields ... */ }) { this.Inner = init?.Inner?.clone() ?? new InnerStruct() // Clones if init.Inner exists, else creates zero value // ... other initializations ... }
- If the corresponding value in
- Constructor Initialization: The generated class constructor accepts an optional
-
Field Access: Accessing struct fields uses standard TypeScript dot notation (
instance.FieldName). Go's automatic dereferencing for pointer field access (ptr.Field) translates to accessing the value with appropriate null checks. Unexported fields become public class members. -
Struct Composite Literals: - Value Initialization (
T{...}): Translates tonew TypeName({...}).go type Point struct{ X, Y int } v := Point{X: 1, Y: 2}becomes:typescript class Point { /* ... constructor, fields, clone ... */ } let v: Point = new Point({ X: 1, Y: 2 })- Pointer Initialization (&T{...}): The implementation of pointer initialization needs to be documented after changes. -
Interfaces: Mapped to TypeScript
interfacetypes. Methods retain their original Go casing. -
Embedded Interfaces: Go interfaces can embed other interfaces. This is translated using TypeScript's
extendskeyword. The generated TypeScript interface extends all the interfaces embedded in the original Go interface.go // Go code type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error } type ReadCloser interface { Reader // Embeds Reader Closer // Embeds Closer }becomes:typescript // TypeScript translation interface Reader { Read(_p0: number[]): [number, $.Error]; } interface Closer { Close(): $.Error; } // ReadCloser extends both Reader and Closer interface ReadCloser extends Reader, Closer { }- Runtime Registration: When registering an interface type with the runtime (
$.registerType), the set of method names includes all methods from the interface itself and all methods from any embedded interfaces.// Example registration for ReadCloser const ReadCloser__typeInfo = $.registerType( 'ReadCloser', $.TypeKind.Interface, null, new Set(['Close', 'Read']), // Includes methods from Reader and Closer undefined );
- Runtime Registration: When registering an interface type with the runtime (
-
Type Assertions: Go's type assertion syntax (
i.(T)) allows checking if an interface variableiholds a value of a specific concrete typeTor implements another interfaceT. This is translated using the$.typeAssertruntime helper function.-
Comma-Ok Assertion (
v, ok := i.(T)): This form checks if the assertion holds and returns the asserted value (or zero value) and a boolean status. Handled in assignment logic.- Interface-to-Concrete Example:
becomes:
// Go code (from compliance/tests/interface_type_assertion) var i MyInterface s := MyStruct{Value: 10} i = s _, ok := i.(MyStruct) // Assert interface 'i' holds concrete type 'MyStruct'
// TypeScript translation import * as $ from "@goscript/builtin"; let i: MyInterface; let s = new MyStruct({ Value: 10 }) i = s.clone() // Assuming MyInterface holds values, clone needed // Assignment logic generates this: let { value: _, ok } = $.typeAssert<MyStruct>(i, 'MyStruct') if (ok) { console.log("Type assertion successful") }
- Interface-to-Interface Example:
becomes:
// Go code (from compliance/tests/embedded_interface_assertion) var rwc ReadCloser s := MyStruct{} // MyStruct implements ReadCloser rwc = s _, ok := rwc.(ReadCloser) // Assert interface 'rwc' holds type 'ReadCloser'
// TypeScript translation import * as $ from "@goscript/builtin"; let rwc: ReadCloser; let s = new MyStruct({ }) rwc = s.clone() // Assuming ReadCloser holds values // Assignment logic generates this: let { value: _, ok } = $.typeAssert<ReadCloser>(rwc, 'ReadCloser') if (ok) { console.log("Embedded interface assertion successful") }
- Translation Details: The Go assertion
v, ok := i.(T)is translated during assignment (WriteStmtAssign) to:- A call to
$.typeAssert<T>(i, 'TypeName').<T>: The target type (interface or class) is passed as a TypeScript generic parameter.'TypeName': The name of the target typeTis passed as a string literal. This string is used by the runtime helper for type checking against registered type information.
- The helper returns an object
{ value: T | null, ok: boolean }. - Object destructuring is used to extract the
valueandokproperties into the corresponding variables from the Go code (e.g.,let { value: v, ok } = ...). If a variable is the blank identifier (_), it's assigned usingvalue: _in the destructuring pattern.
- A call to
- Interface-to-Concrete Example:
-
Panic Assertion (
v := i.(T)): This form asserts thatiholds typeTand panics if it doesn't. Handled in expression logic (WriteTypeAssertExpr). The translation uses the same$.typeAsserthelper but wraps it in an IIFE that checksokand throws an error if false, otherwise returns thevalue.
-
-
Slices: Go slices (
[]T) are mapped to standard TypeScript arrays (T[]) augmented with a hidden__capacityproperty to emulate Go's slice semantics. Runtime helpers from@goscript/builtinare crucial for correct behavior.- Representation: A Go slice is represented in TypeScript as
Array<T> & { __capacity?: number }. The__capacityproperty stores the slice's capacity. - Creation (
make):make([]T, len)andmake([]T, len, cap)are translated using the generic runtime helper$.makeSlice<T>(len, cap?).becomes:s1 := make([]int, 5) // len 5, cap 5 s2 := make([]int, 5, 10) // len 5, cap 10 var s3 []string // nil slice
import * as $ from "@goscript/builtin" let s1 = $.makeSlice<number>(5) // Creates array len 5, sets __capacity = 5 let s2 = $.makeSlice<number>(5, 10) // Creates array len 5, sets __capacity = 10 let s3: string[] = [] // Represents nil slice as empty array
- Literals: Slice literals are translated directly to TypeScript array literals. The capacity of a slice created from a literal is equal to its length.
becomes:
s := []int{1, 2, 3} // len 3, cap 3
let s = [1, 2, 3] // Runtime helpers treat this as having __capacity = 3
- Length (
len(s)): Uses the runtime helper$.len(s). Returns0for nil (empty array) slices. - Capacity (
cap(s)): Uses the runtime helper$.cap(s). This helper reads the__capacityproperty or defaults to the array'slengthif__capacityis not set (e.g., for plain array literals). Returns0for nil (empty array) slices. - Access/Assignment (
s[i]): Translated directly using standard TypeScript array indexing (s[i]). Out-of-bounds access will likely throw a runtime error in TypeScript, similar to Go's panic. - Slicing (
a[low:high],a[low:high:max]): Slicing operations create a new slice header (a new TypeScript array object with its own__capacity) that shares the same underlying data as the original array or slice. This is done using the$.sliceruntime helper.a[low:high]translates to$.slice(a, low, high). The new slice has lengthhigh - lowand capacityoriginal_capacity - low.a[:high]translates to$.slice(a, undefined, high).a[low:]translates to$.slice(a, low, undefined).a[:]translates to$.slice(a, undefined, undefined).a[low:high:max]translates to$.slice(a, low, high, max). The new slice has lengthhigh - lowand capacitymax - low.
becomes:arr := [5]int{0, 1, 2, 3, 4} // Array (len 5, cap 5) s1 := arr[1:4] // [1, 2, 3], len 3, cap 4 (5-1) s2 := s1[1:2] // [2], len 1, cap 3 (cap of s1 - 1) s3 := arr[0:2:3] // [0, 1], len 2, cap 3 (3-0)
Important: Modifications made through a slice affect the underlying data. As demonstrated in the compliance tests (e.g., "Slicing a slice"), changes made via one slice variable (likelet arr = [0, 1, 2, 3, 4] let s1 = $.slice(arr, 1, 4) // len 3, __capacity 4 let s2 = $.slice(s1, 1, 2) // len 1, __capacity 3 let s3 = $.slice(arr, 0, 2, 3) // len 2, __capacity 3
subSlice2modifying index 0) are visible through other slice variables (subSlice1,baseSlice) that share the same underlying memory region. - Append (
append(s, ...)): Translated using the$.appendruntime helper. Crucially, the result of$.appendmust be assigned back to the slice variable, asappendmay return a new slice instance if reallocation occurs.becomes:s = append(s, elem1, elem2) s = append(s, anotherSlice...) // Spread operator
s = $.append(s, elem1, elem2) s = $.append(s, ...anotherSlice) // Spread operator
- Behavior:
- If appending fits within the existing capacity (
len(s) + num_elements <= cap(s)), elements are added to the underlying array, and the original slice header's length is updated (potentially modifying the same objectsrefers to). The underlying array is modified. - If appending exceeds the capacity, a new, larger underlying array is allocated, the existing elements plus the new elements are copied to it, and
appendreturns a new slice header referencing this new array. The original underlying array is not modified beyond its bounds. - Appending to a nil slice allocates a new underlying array.
- If appending fits within the existing capacity (
- Behavior:
- Representation: A Go slice is represented in TypeScript as
-
Arrays: Go arrays (e.g.,
[5]int) have a fixed size known at compile time. They are also mapped to TypeScript arrays (T[]), but their fixed-size nature is enforced during compilation (e.g., preventingappend). Slicing an array (arr[:],arr[low:high], etc.) uses the$.slicehelper, resulting in a Go-style slice backed by the original array data.- Sparse Array Literals: For Go array literals with specific indices (e.g.,
[5]int{1: 10, 3: 30}), unspecified indices are filled with the zero value of the element type in the generated TypeScript. For example,[5]int{1: 10, 3: 30}becomes[0, 10, 0, 30, 0].
- Sparse Array Literals: For Go array literals with specific indices (e.g.,
Note: The distinction between slices and arrays in Go is important. While both often map to TypeScript arrays, runtime helpers (makeSlice, slice, len, cap, append) and the __capacity property are essential for emulating Go's slice semantics accurately.
- Maps: Go maps (
map[K]V) are translated to TypeScript's standardMap<K, V>objects. Various Go map operations are mapped as follows:- Creation (
make):make(map[K]V)is translated using a runtime helper:becomes:m := make(map[string]int)
import * as $ from "@goscript/builtin" let m = $.makeMap<string, number>() // Using generics for type information
- Literals: Map literals are translated to
new Map(...):becomes:m := map[string]int{"one": 1, "two": 2}
let m = new Map([["one", 1], ["two", 2]])
- Assignment (
m[k] = v): Uses a runtime helpermapSet:becomes:m["three"] = 3
$.mapSet(m, "three", 3)
- Access (
m[k]): Uses the standardMap.get()method combined with the nullish coalescing operator (??) to provide the zero value if the key is not found.becomes (simplified conceptual translation):val := m["one"] // Assuming m["one"] exists zero := m["nonexistent"] // Assuming m["nonexistent"] doesn't exist
let val = m.get("one") ?? 0 // Provide zero value (0 for int) if undefined let zero = m.get("nonexistent") ?? 0 // Provide zero value (0 for int) if undefined
- Comma-Ok Idiom (
v, ok := m[k]): Translated usingMap.has()andMap.get()with zero-value handling during assignment:becomes:v, ok := m["three"]
// Assignment logic generates this: let ok: boolean let v: number ok = m.has("three") v = m.get("three") ?? 0 // Provide zero value if key doesn't exist
- Length (
len(m)): Uses a runtime helperlen:becomes:size := len(m)
let size = $.len(m) // Uses runtime helper, not Map.size directly
- Deletion (
delete(m, k)): Uses a runtime helperdeleteMapEntry:becomes:delete(m, "two")
$.deleteMapEntry(m, "two")
- Iteration (
for k, v := range m): Uses standardMap.entries()andfor...of:becomes:for key, value := range m { // ... }
for (const [k, v] of m.entries()) { // ... (key and value are block-scoped) }
@goscript/builtin) is crucial for correctly emulating Go's map semantics, especially regarding zero values and potentially type information formakeMap. - Creation (
- Functions: Converted to TypeScript
functions. Exported functions are prefixed withexport. - Function Literals: Go function literals (anonymous functions) are translated into TypeScript arrow functions (
=>).becomes:greet := func(name string) string { return "Hello, " + name } message := greet("world")
let greet = (name: string): string => { // Arrow function return "Hello, " + name } let message = greet("world")
- Methods: Go functions with receivers are generated as methods within the corresponding TypeScript
class. They retain their original Go casing.- Receiver Type (Value vs. Pointer): Both value receivers (
func (m MyStruct) Method()) and pointer receivers (func (m *MyStruct) Method()) are translated into regular methods on the TypeScript class.becomes:type Counter struct{ count int } func (c Counter) Value() int { return c.count } // Value receiver func (c *Counter) Inc() { c.count++ } // Pointer receiver
class Counter { private count: number = 0; // Receiver 'c' bound to 'this' public Value(): number { const c = this; return c.count; } public Inc(): void { const c = this; c.count++; } // ... constructor, clone ... }
- Method Calls: Go allows calling pointer-receiver methods on values (
value.PtrMethod()) and value-receiver methods on pointers (ptr.ValueMethod()) via automatic referencing/dereferencing. The translation of these in TypeScript needs to be documented after implementation changes. - Receiver Binding: As per Code Generation Conventions, the Go receiver identifier (e.g.,
c) is bound tothiswithin the method body (const c = this). - Semantic Note on Value Receivers: See "Divergences from Go".
- Receiver Type (Value vs. Pointer): Both value receivers (
Go's value semantics (where assigning a struct copies it) are emulated in TypeScript by:
- Adding a
clone()method to generated classes representing structs. This method performs a deep copy.- The
clone()method creates a new instance of the struct and then copies the values from the original struct's_fieldsto the new instance's_fields. For each field, the value is retrieved from the source variable reference (e.g.,this._fields.MyInt.value) and then re-wrapped in a new variable reference in the destination (e.g.,cloned._fields.MyInt = $.varRef(...)). - For nested struct fields, the
clone()method of the nested struct is called recursively (e.g.,cloned._fields.InnerStruct = $.varRef(this._fields.InnerStruct.value?.clone() ?? new MyStruct())).
// Example: MyStruct.clone() public clone(): MyStruct { const cloned = new MyStruct() cloned._fields = { MyInt: $.varRef(this._fields.MyInt.value), MyString: $.varRef(this._fields.MyString.value) } return cloned } // Example: NestedStruct.clone() with nested MyStruct public clone(): NestedStruct { const cloned = new NestedStruct() cloned._fields = { Value: $.varRef(this._fields.Value.value), InnerStruct: $.varRef(this._fields.InnerStruct.value?.clone() ?? new MyStruct()) // Recursive clone } return cloned }
- The
- Automatically calling
.clone()during assignment statements (=,:=) whenever a struct value is being copied.- If the source variable is a direct struct instance (not a variable reference), it's
let valueCopy = original.clone(). - If the source variable is a
$.VarRef<StructType>(because its address was taken), the assignment becomeslet valueCopy = original.value.clone().
// Go original := MyStruct{MyInt: 42} valueCopy := original // (later &original might be used, causing 'original' to be a variable reference in TS)
// TypeScript (assuming 'original' is a variable reference) let original: $.VarRef<MyStruct> = $.varRef(new MyStruct({MyInt: 42})); let valueCopy = original.value.clone();
- If the source variable is a direct struct instance (not a variable reference), it's
- Pointer assignments preserve Go's reference semantics by copying the reference to the
$.VarRefor the direct object reference (for non-variable-reference struct pointers).
Pointer assignments are handled as described under Operators (&, *) and Pointer Representation/Variable References.
Go's multi-assignment statements (where multiple variables are assigned in a single statement) are translated based on the RHS:
- Multiple RHS values:
a, b := val1, val2becomes separate assignmentslet a = compiled_val1; let b = compiled_val2. - Single function call RHS:
a, b := funcReturningTwoValues()becomes destructuring assignmentlet [a, b] = funcReturningTwoValues(). - Map comma-ok RHS:
v, ok := myMap[key]becomes separate assignments forokandvusingMap.hasandMap.get. - Type assertion comma-ok RHS:
v, ok := i.(T)becomes destructuring assignmentlet { value: v, ok } = $.typeAssert(...). - Channel receive comma-ok RHS:
v, ok := <-chbecomes destructuring assignmentconst { value: v, ok } = await ch.receiveWithOk().
The blank identifier (_) in Go results in the omission of the corresponding variable in the TypeScript assignment/destructuring pattern, though the RHS expression is still evaluated for potential side effects.
Go operators are generally mapped directly to their TypeScript equivalents:
- Arithmetic Operators:
+,-,*,/,%map directly. Integer division/is wrapped inMath.floor(). - Comparison Operators:
==,!=for pointers: Map directly to===,!==(reference equality).==,!=for non-pointers: Map directly to===,!==. Struct comparison is reference equality unless specific comparison methods are defined.<,<=,>,>=: Map directly.
- Address Operator (
&):- Taking the address of a variable (
&v) translates to referencing the$.VarRef<T>object associated withv.
var x int p := &x // x becomes a variable reference here
let x: $.VarRef<number> = $.varRef(0) // x declared as VarRef let p: $.VarRef<number> | null = x // p gets reference to x's VarRef
- Taking the address of a variable (
- Dereference Operator (
*):- Dereferencing a pointer (
*p) translates to accessing the.valueproperty of the correspondingVarRef. - Dereferencing a pointer to a struct depends on the context (see
design/VAR_REFS.md). - Multi-level Dereferencing: Involves chaining
.valueaccesses, corresponding to each level of variable reference for the intermediate pointers.// Go (from compliance/tests/varRef/varRef.go) // var x int = 10; var p1 *int = &x; var p2 **int = &p1; var p3 ***int = &p2; // x is $.VarRef<number> // p1 is $.VarRef<$.VarRef<number> | null> // p2 is $.VarRef<$.VarRef<$.VarRef<number> | null> | null> // p3 is $.VarRef<$.VarRef<$.VarRef<number> | null> | null> | null (refers to p2's variable reference) ***p3 = 12 y := ***p3
// TypeScript // ... (declarations as above) p3!.value!.value!.value = 12 let y: number = p3!.value!.value!.value
- Dereferencing a pointer (
- Pointer Assignment:
- Assigning an address to a pointer (
p = &v):- If the pointer variable
pon the left-hand side is not a variable reference, it's assigned the reference tov's$.VarRef.// Case 1: Pointer p is not a variable reference var x int = 10 // x will be a variable reference var p *int // p is not a variable reference p = &x // Assign address of x to p
let x: $.VarRef<number> = $.varRef(10) let p: $.VarRef<number> | null // p is not a VarRef itself p = x // p now holds the reference to x's VarRef
- If the pointer variable
p1on the left-hand side is a variable reference (because&p1was taken elsewhere), its.valueis updated to point tov's$.VarRef.// Case 2: Pointer p1 IS a variable reference // Assume: var p1 $.VarRef<$.VarRef<number> | null> (p1 was made a variable reference) var y int = 15 // y will be a variable reference p1 = &y
// Assume: let p1: $.VarRef<$.VarRef<number> | null> = ...; let y: $.VarRef<number> = $.varRef(15) p1.value = y // Update the inner value of p1's VarRef to point to y's VarRef
- If the pointer variable
- Assigning to a dereferenced pointer (
*p = val): Translates to assigning to the.valueproperty of theVarRefthatp(or the final pointer in a chain) refers to.// Assume p points to x's variable reference: p: $.VarRef<number> | null = x_varRef *p = 100
p!.value = 100 // Assign to the value inside the VarRef p points to
- Assigning an address to a pointer (
- Bitwise Operators:
&,|,^,&^,<<,>>map to TS&,|,^,& ~,<<,>>. Bitwise NOT^xmaps to~x.
Go has a single for construct. We map it to TypeScript's for and while loops.
-
Standard
forloop (init; condition; post):for i := 0; i < 10; i++ { // ... }
Translates directly to a TypeScript
forloop:for (let i = 0; i < 10; i++) { // ... }
Variable scoping within the loop follows Go rules, potentially requiring temporary variables in TypeScript if loop variables are captured in closures.
-
whileloop (condition only):for condition { // ... }
Translates to a TypeScript
whileloop:while (condition) { // ... }
-
Infinite loop:
for { // ... }
Translates to an infinite TypeScript
forloop:for (;;) { // ... }
-
for rangeloop: The Go specification states that the range expression (the collection being iterated over) is evaluated once before the loop begins. The loop iterates over a snapshot of the collection's elements (or at least, its length and elements are fixed).-
Slices:
s := []int{10, 20, 30} for i, v := range s { // ... use i and v } for i := range s { // ... use i } for _, v := range s { // ... use v }
To ensure the "evaluate once" semantic, a helper function
__gs_range(defined in@goscript/builtin) is used. This function takes the slice and returns an iterable yielding[index, value]pairs based on the slice's state when__gs_rangewas called.import { __gs_range } from "@goscript/builtin" const s: number[] = [10, 20, 30] // Assuming slice maps to array // index and value for (const [i, v] of __gs_range(s)) { // ... use i and v } // index only for (const [i] of __gs_range(s)) { // Or potentially optimized helper // ... use i } // value only for (const [, v] of __gs_range(s)) { // ... use v }
-
Arrays: Go arrays have a fixed size. The "evaluate once" semantic applies similarly, meaning the loop iterates over the elements as they were when the loop started, even if the array's elements are modified during iteration.
var a [3]int = [3]int{10, 20, 30} for i, v := range a { // ... use i and v } for i := range a { // ... use i }
To achieve this, a copy of the array is made before the loop begins.
// Assume 'a' is the TypeScript representation of the Go array const __copy_a = [...a] // Create a copy // index and value const __len_a = __copy_a.length // Length evaluated once for (let i = 0; i < __len_a; i++) { const v = __copy_a[i] // Use value from the copy // ... use i and v } // index only // Note: Current implementation uses for...in on the copy for (const i_str in __copy_a) { const i = parseInt(i_str) // Index from string key if (isNaN(i)) { continue } // Skip non-numeric keys if any // ... use i } // Alternative (potentially cleaner): // const __len_a = __copy_a.length // for (let i = 0; i < __len_a; i++) { // // ... use i // } // value only const __len_a_val = __copy_a.length for (let i = 0; i < __len_a_val; i++) { const v = __copy_a[i] // Use value from the copy // ... use v }
The copy ensures that modifications to the original array
aduring the loop do not affect the iteration range or the values yielded by the loop, matching Go's behavior. The index-only iteration currently usesfor...inon the copy; while functional, using a standard indexedforloop might be considered for consistency.
-
break and continue statements are translated directly to their TypeScript counterparts. Labeled break and continue are also supported and map directly to labeled statements in TypeScript.
Go's switch statement is translated into a standard TypeScript switch statement.
-
Basic Switch:
switch value { case 1: // do something case 2, 3: // Multiple values per case // do something else default: // default action }
becomes:
switch (value) { case 1: // do something break // Automatically added case 2: // Multiple Go cases become separate TS cases case 3: // do something else break // Automatically added default: // default action break // Automatically added }
Note:
breakstatements are automatically inserted at the end of each translatedcaseblock to replicate Go's default behavior of not falling through. -
Switch without Expression: A Go
switchwithout an expression (switch { ... }) is equivalent toswitch true { ... }and is useful for cleaner if/else-if chains. This translates similarly, comparingtrueagainst the case conditions.switch { case x < 0: // negative case x == 0: // zero default: // x > 0 // positive }
becomes:
switch (true) { case x < 0: // negative break case x == 0: // zero break default: // positive break }
-
Fallthrough: Go's explicit
fallthroughkeyword is not currently supported and would require specific handling if implemented.
Go's select statement, used for channel communication, is translated using a runtime helper:
select {
case val, ok := <-ch1:
// Process received value
case ch2 <- value:
// Process after sending
default:
// Default case
}becomes:
import * as $ from "@goscript/builtin"
await $.selectStatement([
{
id: 0, // Unique identifier for this case
isSend: false, // This is a receive operation
channel: ch1,
onSelected: async (result) => {
// Assignment logic handles declaration
const { value: val, ok } = result
// Process received value
}
},
{
id: 1, // Unique identifier for this case
isSend: true, // This is a send operation
channel: ch2,
value: value,
onSelected: async () => {
// Process after sending
}
}
], true) // true indicates there's a default caseThe selectStatement helper takes an array of case objects, each containing:
id: A unique identifier for the caseisSend: Boolean indicating whether this is a send (true) or receive (false) operationchannel: The channel to operate onvalue: (For send operations) The value to sendonSelected: Callback function that runs when this case is selected
For receive operations, the callback receives a result object with value and ok properties, similar to Go's comma-ok syntax. The second parameter to selectStatement indicates whether the select has a default case.
Go's if statements are translated into standard TypeScript if statements.
-
Basic
if/else if/else:if condition1 { // block 1 } else if condition2 { // block 2 } else { // block 3 }
becomes:
if (condition1) { // block 1 } else if (condition2) { // block 2 } else { // block 3 }
-
ifwith Short Statement: Go allows an optional short statement (typically variable initialization) before the condition. The scope of variables declared in the short statement is limited to theif(and anyelse if/else) blocks. This is translated by declaring the variable(s) before theifstatement in TypeScript, often within a simple block{}to mimic the limited scope.if v := computeValue(); v > 10 { // use v } else { // use v } // v is not accessible here
becomes:
{ // Block to limit scope let v = computeValue() if (v > 10) { // use v } else { // use v } } // v is not accessible here
Go's zero values are mapped as follows:
number:0string:""boolean:falsestruct:new TypeName()(Value typeT)pointer:nullinterface,slice,map,channel,function:nullor empty equivalent ([],new Map(), etc. depending on context and runtime helpers).
- Go packages are mapped to TypeScript modules under the
@goscript/scope (e.g.,import { MyType } from '@goscript/my/package';). - The GoScript runtime is imported using the
@goscript/builtinalias, which maps to thegs/builtin/index.tsfile. - Standard Go library packages might require specific runtime implementations or shims.
- No Trailing Semicolons: Generated TypeScript code omits semicolons at end of statements. Statements are line-separated without
;.
GoScript handles Go's concurrency primitives (like channels and potentially goroutines in the future) by mapping them to TypeScript's async/await mechanism where appropriate.
To determine which functions need to be marked async in TypeScript, the compiler performs a "function coloring" analysis during compilation:
- Base Cases (Async Roots):
- A function is inherently Asynchronous if its body contains:
- A channel receive operation (
<-ch). - A channel send operation (
ch <- val). - A
selectstatement. - A goroutine creation (
gostatement).
- A channel receive operation (
- A function is inherently Asynchronous if its body contains:
- Propagation:
- A function is marked Asynchronous if it directly calls another function that is already marked Asynchronous.
- Default:
- If a function does not meet any of the asynchronous criteria above, it is considered Synchronous.
The GoScript compiler incorporates a dedicated analysis phase that executes after parsing and type checking but before code generation. This phase performs a comprehensive traversal of the Go Abstract Syntax Tree (AST), leveraging type information provided by the go/packages and go/types libraries. The primary goal is to gather all necessary information about the code's structure, semantics, and potential runtime behavior upfront.
All collected information is stored in a read-only Analysis struct. This ensures that the subsequent code generation phase can focus solely on translating the AST into TypeScript based on pre-computed facts, without needing to perform complex analysis or maintain mutable state during writing.
Key responsibilities of the analysis phase include:
- Processing Imports: Collects and organizes import information, including import paths and aliased names, for use in generating TypeScript import statements.
- Handling Comment Maps: Associates comments with the relevant AST nodes, preserving comments for inclusion in the generated code.
- Analyzing Asynchronous Operations and Defer Statements:
- Identifies which functions (including function literals) are asynchronous based on the presence of channel operations,
selectstatements, goroutine creations, or calls to other asynchronous functions. This "function coloring" is essential for generating correctasync/awaitcode. - Determines which code blocks contain
deferstatements. - Specifically identifies if a
deferstatement refers to an asynchronous function literal. This information is used to decide whether to useawait using $.AsyncDisposableStack()for the block and generate anasync () => { ... }callback for the deferred function.
- Identifies which functions (including function literals) are asynchronous based on the presence of channel operations,
- Analyzing Unexported Field Access: Unexported fields of structs are translated as public fields in TypeScript. Go's package-level visibility for unexported fields is not strictly enforced in the generated TypeScript; all fields become public.
By performing these analyses ahead of time, the compiler simplifies the code generation process and improves the overall correctness and maintainability of the generated TypeScript code.
Channel operations are translated as follows:
- Creation:
make(chan T, capacity)is translated to$.makeChannel<T>(capacity, zeroValueOfTypeT). For unbuffered channels (make(chan T)), the capacity is0. - Receive:
val := <-chis translated toval = await ch.receive(). - Receive (comma-ok):
val, ok := <-chis translated toconst { value: val, ok } = await ch.receiveWithOk(). - Send:
ch <- valis translated toawait ch.send(val). - Close:
close(ch)is translated toch.close().
Go's goroutine creation (go func() { ... }()) is translated to a call to queueMicrotask with the target function wrapped in an async arrow function:
go func() {
// Goroutine body
}()becomes:
queueMicrotask(async () => {
{
// Goroutine body
}
})- Async Functions: Go functions colored as Asynchronous are generated as TypeScript
async functions. Their return typeTis wrapped in aPromise<T>. If the function has no return value, the TypeScript return type isPromise<void>. - Sync Functions: Go functions colored as Synchronous are generated as regular TypeScript
functions with their corresponding return types. - Function Calls: When a Go function call targets an Asynchronous function, the generated TypeScript call expression is prefixed with the
awaitkeyword. Calls to Synchronous functions are generated directly withoutawait.
This coloring approach ensures that asynchronous operations propagate correctly through the call stack in the generated TypeScript code.
Consider the following Go code using a channel:
package main
// This function receives from a channel, making it async.
func receiveFromChan(ch chan int) int {
val := <-ch // This operation makes the function async
return val
}
// This function calls an async function, making it async too.
func caller(ch chan int) int {
// We expect this call to be awaited in TypeScript
result := receiveFromChan(ch)
return result + 1
}
func main() {
myChan := make(chan int, 1)
myChan <- 10
finalResult := caller(myChan)
println(finalResult) // Expected output: 11
close(myChan)
}This translates to the following TypeScript:
import * as $ from "@goscript/builtin";
// Marked async because it contains 'await ch.receive()'
async function receiveFromChan(ch: $.Channel<number>): Promise<number> {
let val = await ch.receive()
return val
}
// Marked async because it calls the async 'receiveFromChan'
async function caller(ch: $.Channel<number>): Promise<number> {
let result = await receiveFromChan(ch)
return result + 1
}
// Marked async because it calls the async 'caller' and uses 'await myChan.send()'
export async function main(): Promise<void> {
let myChan = $.makeChannel<number>(1, 0)
await myChan.send(10) // Send is awaited
let finalResult = await caller(myChan)
console.log(finalResult)
myChan.close()
}Note on Microtasks: While Go's concurrency model involves goroutines and a scheduler, the TypeScript translation primarily uses async/await and Promises for channel operations. Starting a new Goroutine with the go keyword is translated to a call to queueMicrotask with the target function, scheduling it to run asynchronously.