Skip to content

Commit b49c2d8

Browse files
committed
feat: implement slices sort func
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent e4c0c04 commit b49c2d8

File tree

22 files changed

+451
-8
lines changed

22 files changed

+451
-8
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This document contains guidelines and rules for AI agents working on the GoScrip
1717
- Leverage adding more tests, for example in `compiler/analysis_test.go`, instead of debug logging, for diagnosing issues or investigating hypotheses. If the new test case is temporary and you plan to remove it later, add a `tmp_test.go` file or similar to keep things separated.
1818
- AVOID type arguments unless necessary (prefer type inference)
1919
- When making Git commits use the existing commit message pattern and Linux-kernel style commit message bodies.
20+
- When you would normally add a new compliance test check if a very-similar compliance test already exists and if so extend that one instead. For example testing another function in the same package.
2021

2122
## Project Overview
2223

compiler/composite-lit.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ func (c *GoToTSCompiler) WriteCompositeLit(exp *ast.CompositeLit) error {
341341
case *types.Map, *types.Struct:
342342
// Handle struct directly with the struct literal logic
343343
if structType, ok := underlying.(*types.Struct); ok {
344-
return c.writeUntypedStructLiteral(exp, structType) // true = anonymous
344+
return c.writeUntypedStructLiteral(exp, tv.Type, structType)
345345
}
346346
// Map case would be handled here
347347
return fmt.Errorf("untyped map composite literals not yet supported")
@@ -356,7 +356,7 @@ func (c *GoToTSCompiler) WriteCompositeLit(exp *ast.CompositeLit) error {
356356
// This is an anonymous struct literal with inferred pointer type
357357
// Just create the struct object directly - no var-refing needed
358358
// Anonymous literals are not variables, so they don't get var-refed
359-
return c.writeUntypedStructLiteral(exp, elemType) // true = anonymous
359+
return c.writeUntypedStructLiteral(exp, ptrType.Elem(), elemType)
360360
default:
361361
return fmt.Errorf("unhandled pointer composite literal element type: %T", elemType)
362362
}
@@ -384,7 +384,7 @@ func (c *GoToTSCompiler) writeUntypedArrayLiteral(exp *ast.CompositeLit) error {
384384
}
385385

386386
// writeUntypedStructLiteral handles untyped composite literals that are structs or pointers to structs
387-
func (c *GoToTSCompiler) writeUntypedStructLiteral(exp *ast.CompositeLit, structType *types.Struct) error {
387+
func (c *GoToTSCompiler) writeUntypedStructLiteral(exp *ast.CompositeLit, actualType types.Type, structType *types.Struct) error {
388388
// Create field mapping like the typed struct case
389389
directFields := make(map[string]ast.Expr)
390390

@@ -408,8 +408,23 @@ func (c *GoToTSCompiler) writeUntypedStructLiteral(exp *ast.CompositeLit, struct
408408
}
409409
}
410410

411-
// Write the object literal (always anonymous for untyped)
412-
c.tsw.WriteLiterally("{")
411+
// Check if this is a named type
412+
isNamed := false
413+
if _, ok := actualType.(*types.Named); ok {
414+
isNamed = true
415+
}
416+
417+
// Write the object literal
418+
if isNamed {
419+
// For named structs, use constructor
420+
c.tsw.WriteLiterally("$.markAsStructValue(new ")
421+
// Write the type name
422+
c.WriteGoType(actualType, GoTypeContextGeneral)
423+
c.tsw.WriteLiterally("({")
424+
} else {
425+
// For truly anonymous structs, just write a simple object literal
426+
c.tsw.WriteLiterally("{")
427+
}
413428

414429
firstFieldWritten := false
415430
// Write fields in order
@@ -434,7 +449,12 @@ func (c *GoToTSCompiler) writeUntypedStructLiteral(exp *ast.CompositeLit, struct
434449
firstFieldWritten = true
435450
}
436451

437-
c.tsw.WriteLiterally("}")
452+
// Close the object literal
453+
if isNamed {
454+
c.tsw.WriteLiterally("}))")
455+
} else {
456+
c.tsw.WriteLiterally("}")
457+
}
438458
return nil
439459
}
440460

compliance/WIP.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Analysis: Untyped Struct Literals in Slice Literals
2+
3+
## Problem
4+
5+
When we have a slice literal like:
6+
```go
7+
people := []Person{
8+
{Name: "Charlie", Age: 30},
9+
{Name: "Alice", Age: 25},
10+
{Name: "Bob", Age: 35},
11+
}
12+
```
13+
14+
The generated TypeScript is:
15+
```ts
16+
let people = $.arrayToSlice<Person>([{Age: 30, Name: "Charlie"}, {Age: 25, Name: "Alice"}, {Age: 35, Name: "Bob"}])
17+
```
18+
19+
This generates TypeScript type errors because `{Age: 30, Name: "Charlie"}` is a plain object literal, not a `Person` instance with `_fields` and `clone` methods.
20+
21+
## Root Cause
22+
23+
In `WriteCompositeLit`, when processing array elements at lines 184-186:
24+
```go
25+
if elm, ok := elements[i]; ok && elm != nil {
26+
if err := c.WriteVarRefedValue(elm); err != nil {
27+
return fmt.Errorf("failed to write array literal element: %w", err)
28+
}
29+
}
30+
```
31+
32+
The elements are untyped composite literals (`{Name: "Charlie", Age: 30}`). When `WriteVarRefedValue` is called on these, it delegates to `WriteValueExpr`, which calls `WriteCompositeLit`.
33+
34+
Inside `WriteCompositeLit` for these untyped literals, the function checks if `exp.Type` is nil (line 334). Since these literals don't have an explicit type in the AST, it falls through to the untyped path starting at line 336.
35+
36+
The untyped path calls `writeUntypedStructLiteral` (line 387) which just creates a plain object literal `{...}` instead of calling the struct constructor.
37+
38+
## Solution
39+
40+
When we have an untyped struct literal, we need to check if the inferred type is a named struct type. If it is, we should generate `new StructName({...})` instead of just `{...}`.
41+
42+
The type information is available via `c.pkg.TypesInfo.Types[exp]` which gives us the inferred type.
43+
44+
## Fix
45+
46+
Modify `writeUntypedStructLiteral` to:
47+
1. Check if the inferred type for the expression is a named struct type
48+
2. If so, generate `new TypeName({...})` with the constructor call
49+
3. If it's truly anonymous (no named type), keep the plain object literal
50+
51+
This requires checking the parent types.Type that led us to call writeUntypedStructLiteral.

compliance/deps/encoding/json/encode.gs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2015,7 +2015,7 @@ export function appendString<Bytes extends $.Bytes | string>(dst: $.Bytes, src:
20152015
export async function typeFields(t: reflect.Type): Promise<structFields> {
20162016
// Anonymous fields to explore at the current level and the next.
20172017
let current = $.arrayToSlice<field>([])
2018-
let next = $.arrayToSlice<field>([{typ: t}])
2018+
let next = $.arrayToSlice<field>([$.markAsStructValue(new field({typ: t}))])
20192019

20202020
// Count of queued names for current level and the next.
20212021
let count: Map<reflect.Type, number> | null = null
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
s == nil: true
2+
s[0:0] == nil: true
3+
s[:0] == nil: true
4+
s[:] == nil: true
5+
slice_nil test passed

compliance/tests/slice_nil/index.ts

Whitespace-only changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
func main() {
4+
var s []int
5+
println("s == nil:", s == nil)
6+
7+
// Slicing nil with valid bounds should work
8+
s2 := s[0:0]
9+
println("s[0:0] == nil:", s2 == nil)
10+
11+
s3 := s[:0]
12+
println("s[:0] == nil:", s3 == nil)
13+
14+
s4 := s[:]
15+
println("s[:] == nil:", s4 == nil)
16+
17+
println("slice_nil test passed")
18+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Generated file based on slice_nil.go
2+
// Updated when compliance tests are re-run, DO NOT EDIT!
3+
4+
import * as $ from "@goscript/builtin/index.js"
5+
6+
export async function main(): Promise<void> {
7+
let s: $.Slice<number> = null
8+
console.log("s == nil:", s == null)
9+
10+
// Slicing nil with valid bounds should work
11+
let s2 = $.goSlice(s, 0, 0)
12+
console.log("s[0:0] == nil:", s2 == null)
13+
14+
let s3 = $.goSlice(s, undefined, 0)
15+
console.log("s[:0] == nil:", s3 == null)
16+
17+
let s4 = $.goSlice(s, undefined, undefined)
18+
console.log("s[:] == nil:", s4 == null)
19+
20+
console.log("slice_nil test passed")
21+
}
22+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"compilerOptions": {
3+
"allowImportingTsExtensions": true,
4+
"lib": [
5+
"es2022",
6+
"esnext.disposable",
7+
"dom"
8+
],
9+
"module": "nodenext",
10+
"moduleResolution": "nodenext",
11+
"noEmit": true,
12+
"paths": {
13+
"*": [
14+
"./*"
15+
],
16+
"@goscript/*": [
17+
"../../../gs/*",
18+
"../../../compliance/deps/*"
19+
],
20+
"@goscript/github.com/aperturerobotics/goscript/compliance/tests/slice_nil/*": [
21+
"./*"
22+
]
23+
},
24+
"sourceMap": true,
25+
"target": "es2022"
26+
},
27+
"extends": "../../../tsconfig.json",
28+
"include": [
29+
"index.ts",
30+
"slice_nil.gs.ts"
31+
]
32+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Alice 25
2+
Charlie 30
3+
Bob 35

0 commit comments

Comments
 (0)