Skip to content

Commit ee8a381

Browse files
committed
feat: rune literal and string rune conversion fix
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent fe84c71 commit ee8a381

12 files changed

Lines changed: 140 additions & 75 deletions

File tree

builtin/builtin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,15 @@ export const deleteMapEntry = <K, V>(map: Map<K, V>, key: K): void => {
9292
export const mapHas = <K, V>(map: Map<K, V>, key: K): boolean => {
9393
return map.has(key);
9494
};
95+
/**
96+
* Appends elements to a slice (TypeScript array).
97+
* Note: In Go, append can return a new slice if the underlying array is reallocated.
98+
* This helper emulates that by returning the modified array.
99+
* @param slice The slice (TypeScript array) to append to.
100+
* @param elements The elements to append.
101+
* @returns The modified slice (TypeScript array).
102+
*/
103+
export const append = <T>(slice: Array<T>, ...elements: T[]): Array<T> => {
104+
slice.push(...elements);
105+
return slice;
106+
};

compiler/compile_expr.go

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"go/ast"
77
"go/token"
88
gtypes "go/types"
9+
"strconv"
910

1011
gstypes "github.com/paralin/goscript/types"
1112
)
@@ -383,21 +384,62 @@ func (c *GoToTSCompiler) WriteCallExpr(exp *ast.CallExpr) error {
383384
case "string":
384385
// Handle string() conversion, specifically for rune to string
385386
if len(exp.Args) == 1 {
386-
// Check if the argument is a rune (int32)
387-
if tv, ok := c.pkg.TypesInfo.Types[exp.Args[0]]; ok {
387+
// Check if the argument is a rune (int32) or a call to rune()
388+
arg := exp.Args[0]
389+
innerCall, isCallExpr := arg.(*ast.CallExpr)
390+
391+
if isCallExpr {
392+
// Check if it's a call to rune()
393+
if innerFunIdent, innerFunIsIdent := innerCall.Fun.(*ast.Ident); innerFunIsIdent && innerFunIdent.String() == "rune" {
394+
// Translate string(rune(val)) to String.fromCharCode(val)
395+
if len(innerCall.Args) == 1 {
396+
c.tsw.WriteLiterally("String.fromCharCode(")
397+
if err := c.WriteValueExpr(innerCall.Args[0]); err != nil {
398+
return fmt.Errorf("failed to write argument for string(rune) conversion: %w", err)
399+
}
400+
c.tsw.WriteLiterally(")")
401+
return nil // Handled string(rune)
402+
}
403+
}
404+
}
405+
406+
// Handle direct string(int32) conversion
407+
// This assumes 'rune' is int32
408+
if tv, ok := c.pkg.TypesInfo.Types[arg]; ok {
388409
if basic, isBasic := tv.Type.Underlying().(*gtypes.Basic); isBasic && basic.Kind() == gtypes.Int32 {
389410
// Translate string(rune_val) to String.fromCharCode(rune_val)
390411
c.tsw.WriteLiterally("String.fromCharCode(")
391-
if err := c.WriteValueExpr(exp.Args[0]); err != nil {
392-
return fmt.Errorf("failed to write argument for string(rune) conversion: %w", err)
412+
if err := c.WriteValueExpr(arg); err != nil {
413+
return fmt.Errorf("failed to write argument for string(int32) conversion: %w", err)
393414
}
394415
c.tsw.WriteLiterally(")")
395-
return nil // Handled string(rune)
416+
return nil // Handled string(int32)
396417
}
397418
}
398419
}
399420
// Return error for other unhandled string conversions
400421
return fmt.Errorf("unhandled string conversion: %s", exp.Fun)
422+
case "append":
423+
// Translate append(slice, elements...) to goscript.append(slice, elements...)
424+
if len(exp.Args) >= 1 {
425+
c.tsw.WriteLiterally("goscript.append(")
426+
// The first argument is the slice
427+
if err := c.WriteValueExpr(exp.Args[0]); err != nil {
428+
return fmt.Errorf("failed to write slice in append call: %w", err)
429+
}
430+
// The remaining arguments are the elements to append
431+
for i, arg := range exp.Args[1:] {
432+
if i > 0 || len(exp.Args) > 1 { // Add comma before elements if there are any
433+
c.tsw.WriteLiterally(", ")
434+
}
435+
if err := c.WriteValueExpr(arg); err != nil {
436+
return fmt.Errorf("failed to write argument %d in append call: %w", i+1, err)
437+
}
438+
}
439+
c.tsw.WriteLiterally(")")
440+
return nil // Handled append
441+
}
442+
return errors.New("unhandled append call with incorrect number of arguments")
401443
default:
402444
// Not a special built-in, treat as a regular function call
403445
if err := c.WriteValueExpr(expFun); err != nil {
@@ -480,7 +522,20 @@ func (c *GoToTSCompiler) WriteBinaryExprValue(exp *ast.BinaryExpr) error {
480522

481523
// WriteBasicLitValue writes a basic literal value.
482524
func (c *GoToTSCompiler) WriteBasicLitValue(exp *ast.BasicLit) {
483-
c.tsw.WriteLiterally(exp.Value)
525+
if exp.Kind == token.CHAR {
526+
// Go char literal 'x' is a rune (int32). Translate to its numeric code point.
527+
// Use strconv.UnquoteChar to handle escape sequences correctly.
528+
val, _, _, err := strconv.UnquoteChar(exp.Value[1:len(exp.Value)-1], '\'')
529+
if err != nil {
530+
c.tsw.WriteCommentInline(fmt.Sprintf("error parsing char literal %s: %v", exp.Value, err))
531+
c.tsw.WriteLiterally("0") // Default to 0 on error
532+
} else {
533+
c.tsw.WriteLiterally(fmt.Sprintf("%d", val))
534+
}
535+
} else {
536+
// Other literals (INT, FLOAT, STRING, IMAG)
537+
c.tsw.WriteLiterally(exp.Value)
538+
}
484539
}
485540

486541
/*

compiler/compile_spec.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ func (c *GoToTSCompiler) WriteValueSpec(a *ast.ValueSpec) error {
190190
if a.Type != nil {
191191
c.tsw.WriteLiterally(": ")
192192
c.WriteTypeExpr(a.Type) // Variable type annotation
193+
194+
// Check if it's an array type declaration without an initial value
195+
if _, isArrayType := a.Type.(*ast.ArrayType); isArrayType && len(a.Values) == 0 {
196+
c.tsw.WriteLiterally(" = []")
197+
}
193198
}
194199
if len(a.Values) > 0 {
195200
c.tsw.WriteLiterally(" = ")

compiler/compile_stmt.go

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -370,16 +370,49 @@ func (c *GoToTSCompiler) WriteStmtBlock(exp *ast.BlockStmt, suppressNewline bool
370370
// writeAssignmentCore writes the core LHS, operator, and RHS of an assignment.
371371
// It does NOT handle blank identifiers, 'let' keyword, or trailing semicolons/comments/newlines.
372372
func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Token) error {
373+
// Special handling for integer division assignment (/=)
374+
if tok == token.QUO_ASSIGN && len(lhs) == 1 && len(rhs) == 1 {
375+
lhsType := c.pkg.TypesInfo.TypeOf(lhs[0])
376+
rhsType := c.pkg.TypesInfo.TypeOf(rhs[0])
377+
378+
if lhsType != nil && rhsType != nil {
379+
lhsBasic, lhsIsBasic := lhsType.Underlying().(*gtypes.Basic)
380+
rhsBasic, rhsIsBasic := rhsType.Underlying().(*gtypes.Basic)
381+
382+
if lhsIsBasic && rhsIsBasic && (lhsBasic.Info()&gtypes.IsInteger != 0) && (rhsBasic.Info()&gtypes.IsInteger != 0) {
383+
// Integer division assignment: lhs = Math.floor(lhs / rhs)
384+
if err := c.WriteValueExpr(lhs[0]); err != nil {
385+
return err
386+
}
387+
c.tsw.WriteLiterally(" = Math.floor(")
388+
if err := c.WriteValueExpr(lhs[0]); err != nil { // Write LHS again for the division
389+
return err
390+
}
391+
c.tsw.WriteLiterally(" / ")
392+
if err := c.WriteValueExpr(rhs[0]); err != nil {
393+
return err
394+
}
395+
c.tsw.WriteLiterally(")")
396+
return nil // Handled integer division assignment
397+
}
398+
}
399+
}
400+
401+
// --- Original logic for other assignments ---
402+
isMapIndexLHS := false // Track if the first LHS is a map index
373403
for i, l := range lhs {
374404
if i != 0 {
375405
c.tsw.WriteLiterally(", ")
376406
}
377407
// Handle map indexing assignment specially
378-
isMapIndexLHS := false
408+
currentIsMapIndex := false
379409
if indexExpr, ok := l.(*ast.IndexExpr); ok {
380410
if tv, ok := c.pkg.TypesInfo.Types[indexExpr.X]; ok {
381411
if _, isMap := tv.Type.Underlying().(*gtypes.Map); isMap {
382-
isMapIndexLHS = true
412+
currentIsMapIndex = true
413+
if i == 0 {
414+
isMapIndexLHS = true
415+
}
383416
// Use mapSet helper
384417
c.tsw.WriteLiterally("goscript.mapSet(")
385418
if err := c.WriteValueExpr(indexExpr.X); err != nil { // Map
@@ -395,14 +428,15 @@ func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Toke
395428
}
396429
}
397430

398-
if !isMapIndexLHS {
431+
if !currentIsMapIndex {
399432
if err := c.WriteValueExpr(l); err != nil { // LHS is a value
400433
return err
401434
}
402435
}
403436
}
404-
// Only write the assignment operator for regular variables, not for map assignments
405-
if len(lhs) == 1 && isLHSMapIndex(lhs[0], c.pkg) {
437+
438+
// Only write the assignment operator for regular variables, not for map assignments handled by mapSet
439+
if isMapIndexLHS && len(lhs) == 1 { // Only skip operator if it's a single map assignment
406440
// Continue, we've already written part of the mapSet() function call
407441
} else {
408442
c.tsw.WriteLiterally(" ")
@@ -415,6 +449,7 @@ func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Toke
415449
}
416450
c.tsw.WriteLiterally(" ")
417451
}
452+
418453
for i, r := range rhs {
419454
if i != 0 {
420455
c.tsw.WriteLiterally(", ")
@@ -432,12 +467,11 @@ func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Toke
432467
}
433468
}
434469
// If the LHS was a single map index, close the mapSet call
435-
if len(lhs) == 1 && isLHSMapIndex(lhs[0], c.pkg) {
470+
if isMapIndexLHS && len(lhs) == 1 {
436471
c.tsw.WriteLiterally(")")
437472
}
438473
return nil
439474
}
440-
441475
func (c *GoToTSCompiler) WriteStmtAssign(exp *ast.AssignStmt) error {
442476
// writeTypeAssertion handles multi-variable assignment from a type assertion.
443477
writeTypeAssertion := func(typeAssertExpr *ast.TypeAssertExpr) error {

compliance/.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
2-
/gobyexample
1+
WIP.md
2+
/gobyexample

compliance/COMPLIANCE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The following tests are currently implemented in the `/compliance/tests` directo
1414
* **`if_statement/`**: Covers basic `if`/`else` conditional statements, including correct `} else {` formatting.
1515
* **`switch_statement/`**: Verifies basic `switch` statements with integer and string tags and default cases.
1616
* **`for_loop_basic/`**: Verifies basic counter-based `for` loops (`for init; cond; post {}`).
17+
* **`map_support/`**: Covers map creation (`make`, literal), access, assignment, deletion, length, and iteration (`range`).
1718
* **`method_call_on_pointer_receiver/`**: Verifies calling methods with pointer receivers (`*T`) on pointer variables.
1819
* **`method_call_on_value_receiver/`**: Verifies calling methods with value receivers (`T`) on value variables. (Note: Go often implicitly takes the address for pointer receivers, this tests the explicit value receiver case).
1920
* **`pointer_deref_multiassign/`**: Tests dereferencing a pointer during a multi-variable assignment (`:=` or `=`), including the use of the blank identifier (`_`).
@@ -46,6 +47,7 @@ Based on the existing tests, GoScript aims to support the following Go features:
4647
* **Data Structures:**
4748
* Arrays (`[N]T`) - Including array literals and indexing.
4849
* Slices (`[]T`) - Creation using `make([]T, len)` and `make([]T, len, cap)`.
50+
* Maps (`map[K]V`) - Creation using `make`, literals, access, assignment, `delete`, `len`, `range`.
4951
* `struct` definitions (including exported/unexported fields).
5052
* Composite Literals for structs (`MyStruct{...}`).
5153
* **Functions & Methods:**
@@ -80,7 +82,6 @@ The following Go constructs, present in the "Go By Example" guide, do not appear
8082
* `defer` statement
8183
* `panic` / `recover`
8284
* **Data Structures:**
83-
* Maps (`map[K]V`, `make`, `delete`)
8485
* Struct Embedding
8586
* **Functions:**
8687
* Variadic functions (`...T`)

compliance/WIP.md

Lines changed: 0 additions & 50 deletions
This file was deleted.

compliance/compliance.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,20 @@ func WriteTypeScriptRunner(t *testing.T, tempDir string) string {
186186
}
187187

188188
// RunTypeScriptRunner runs the runner.ts file using tsx and returns its stdout.
189-
func RunTypeScriptRunner(t *testing.T, tempDir, tsRunner string) string {
189+
func RunTypeScriptRunner(t *testing.T, workspaceDir, tempDir, tsRunner string) string {
190190
t.Helper()
191191
cmd := exec.Command("tsx", tsRunner)
192192
cmd.Dir = tempDir
193+
194+
// Prepend node_modules/.bin to PATH
195+
nodeBinDir := filepath.Join(workspaceDir, "node_modules", ".bin")
196+
currentPath := os.Getenv("PATH")
197+
newPath := fmt.Sprintf("%s%c%s", nodeBinDir, os.PathListSeparator, currentPath)
198+
cmd.Env = append(os.Environ(), "PATH="+newPath) // Set the modified PATH
199+
193200
var outBuf, errBuf bytes.Buffer
194201
cmd.Stdout = io.MultiWriter(&outBuf, os.Stderr)
195-
cmd.Stderr = os.Stderr
202+
cmd.Stderr = os.Stderr // Keep stderr going to the test output for debugging
196203
if err := cmd.Run(); err != nil {
197204
t.Fatalf("run failed: %v\nstderr: %s", err, errBuf.String())
198205
}
@@ -271,7 +278,7 @@ func RunGoScriptTestDir(t *testing.T, workspaceDir, testDir string) {
271278
}
272279

273280
tsRunner := WriteTypeScriptRunner(t, tempDir)
274-
actual := strings.TrimSpace(RunTypeScriptRunner(t, tempDir, tsRunner))
281+
actual := strings.TrimSpace(RunTypeScriptRunner(t, workspaceDir, tempDir, tsRunner)) // Pass workspaceDir
275282

276283
expectedLogPath := filepath.Join(testDir, "expected.log")
277284
expected, err := os.ReadFile(expectedLogPath)

compliance/tests/map_support/map_support.gs.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function main(): void {
4545
// Iterate over a map with range
4646
console.log("Iterating over scores map:")
4747
// Note: Map iteration is not ordered in Go, so we will collect the results and sort them for consistent test output.
48-
let scoreResults: string[];
48+
let scoreResults: string[] = [];
4949

5050
// Using string concatenation to build the output string
5151
for (const [k, v] of scores.entries()) {
@@ -54,7 +54,7 @@ export function main(): void {
5454
{
5555
// Using string concatenation to build the output string
5656
let result = " - Name: " + name + " Score: " + itoa(score)
57-
scoreResults = append(scoreResults, result)
57+
scoreResults = goscript.append(scoreResults, result)
5858
}
5959
}
6060

@@ -91,8 +91,8 @@ function itoa(i: number): string {
9191
i = -i
9292
}
9393
for (; i > 0; ) {
94-
s = string(rune(i % 10 + '0')) + s
95-
i /= 10
94+
s = String.fromCharCode(i % 10 + 48) + s
95+
i = Math.floor(i / 10)
9696
}
9797
if (isNegative) {
9898
s = "-" + s
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)