From 860e2e299065fd246323d328a30d3131e5144197 Mon Sep 17 00:00:00 2001 From: Pham Tan Minh Tien Date: Thu, 24 Sep 2026 09:13:17 +0700 Subject: [PATCH 1/2] feat(stdlib,lowering): implement WHATWG IterableIterator and generic Array.from/for..of iterator support --- docs/typescript-parity-report.md | 4 +- internal/lowering/builtins_array.go | 290 ++++++++++++++++++++-- internal/lowering/generics.go | 2 + internal/lowering/generics_subst.go | 108 +++++++- internal/lowering/statements_control.go | 101 ++++++-- internal/lowering/type_aliases.go | 37 +++ internal/typescriptgo/stdlib/formdata.ts | 27 +- internal/typescriptgo/stdlib/globals.d.ts | 39 ++- internal/typescriptgo/stdlib/http.ts | 56 +++-- internal/typescriptgo/stdlib/url.ts | 30 +-- 10 files changed, 595 insertions(+), 99 deletions(-) create mode 100644 internal/lowering/type_aliases.go diff --git a/docs/typescript-parity-report.md b/docs/typescript-parity-report.md index 5a70b36..66fcaa5 100644 --- a/docs/typescript-parity-report.md +++ b/docs/typescript-parity-report.md @@ -169,7 +169,7 @@ All test cases in the regression test suite (Corpus Test Suite) have been cross- | **`node:test` / `test`** | `test`, `it`, `describe`, `suite`, `before`, `after`, `beforeEach`, `afterEach`, `mock`, `TestContext`, `SuiteContext`, subtests, TAP/spec reporting | ✅ Matches Node.js test runner core subset specification | | **`node:dgram` / `dgram`** | `Socket` class (`bind`, `connect`, `disconnect`, `send`, `close`, `address`, `remoteAddress`, `setBroadcast`, `setTTL`, `setMulticastTTL`, `setMulticastLoopback`, `setMulticastInterface`, `addMembership`, `dropMembership`, `addSourceSpecificMembership`, `dropSourceSpecificMembership`, `setRecvBufferSize`, `setSendBufferSize`, `[Symbol.asyncDispose]`), `createSocket` | ✅ Matches Node.js dgram UDP core subset specification | | **`node:http` & WHATWG Fetch** | `Server`, `createServer`, `IncomingMessage`, `OutgoingMessage`, `ServerResponse`, `ClientRequest`, `Agent`, `globalAgent`, `request`, `get`, `fetch`, `Request`, `Response`, `Headers`, `Response.formData`, `METHODS`, `STATUS_CODES`, `getStatusText`, `maxHeaderSize`, `validateHeaderName`, `validateHeaderValue` | ✅ Full Node.js HTTP server/client engine on `net.Server` / `net.Socket` and WHATWG Fetch | -| **WHATWG FormData** | `FormData` (`append`, `delete`, `get`, `getAll`, `has`, `set`, `forEach`, `entries`, `keys`, `values`, `[Symbol.iterator]()`), `FormDataEntryValue`, Blob/File normalization with filename override, and `Response.prototype.formData()` multipart/form-data and urlencoded parser | ✅ Full WHATWG standard FormData and Fetch multipart/urlencoded body decoder | +| **WHATWG FormData** | `FormData` (`append`, `delete`, `get`, `getAll`, `has`, `set`, `forEach`, `entries`, `keys`, `values`, `[Symbol.iterator]()`), `FormDataIterator`, `FormDataEntryValue`, Blob/File normalization with filename override, and `Response.prototype.formData()` multipart/form-data and urlencoded parser | ✅ Full WHATWG standard FormData with `IterableIterator` protocol, generic `for..of` and `Array.from` iterator support, and Fetch multipart/urlencoded body decoder | | **`node:net`** | `isIP`, `isIPv4`, `isIPv6`, `Socket`, `Server`, `SocketAddress`, `createServer`, `createConnection`, `connect` | ✅ Matches Node.js Net POSIX TCP socket specification | | **`Weak Collections, WeakRef & FinalizationRegistry`** | `WeakMap`, `WeakSet`, `WeakRef` (`.deref()`), `FinalizationRegistry` (`.register()`, `.unregister()`), `gc()`, Cycle-Aware Mark-and-Sweep Memory Management | ✅ 100% matches ECMAScript Weak Collections, weak references & finalizers | | **`node:stream` / `stream`** | `Stream`, `Readable`, `Writable`, `Duplex`, `Transform`, `PassThrough`, `pipeline`, `finished`, `compose`, `addAbortSignal`, `getDefaultHighWaterMark`, `setDefaultHighWaterMark`, `isReadable`, `isWritable`, `isErrored`, `Readable.from`, `Readable.isDisturbed`, WebStreams interop (`fromWeb`, `toWeb`, `duplexFromWeb`, `duplexToWeb`), `promises`; `node:stream/consumers` (`buffer`, `text`, `json`, `arrayBuffer`, `blob`) | ✅ Matches Node.js Stream core subset specification | @@ -383,7 +383,7 @@ Below is the detailed audit of all TypeScript/ECMAScript Abstract Syntax Tree (A | **URL & URLSearchParams (`node:url`)** | ✅ Core APIs | `URL`, `URLSearchParams`, `Url`, `parse`, `format`, `resolve`, `fileURLToPath`, `pathToFileURL`, `urlToHttpOptions`, and iterator/property methods. Dummy `createObjectURL` and `domainToASCII`/`domainToUnicode` stubs removed. | | **SQLite Embedded Database (`node:sqlite`)** | ✅ Full (32 / 32 APIs) | Full core subset parity: `DatabaseSync`, `StatementSync`, `Session`, `backup`, `constants` (`SQLITE_OPEN_*`, `SQLITE_CHANGESETAPPLY_*`), `exec`, `prepare`, `function`, `aggregate`, `loadExtension`, `enableLoadExtension`, `createSession`, `applyChangeset`, `all`, `get`, `run`, `iterate`, `columns`, `expandedSQL`, `sourceSQL`, `[Symbol.dispose]`. | | **Global Identifiers & Web Globals (`node:globals`)** | ✅ Core APIs | Genuine Web & Node.js globals: `AbortController`, `AbortSignal`, `Blob`, `Buffer`, `ByteLengthQueuingStrategy`, `CompressionStream`, `CountQueuingStrategy`, `Crypto`, `CryptoKey`, `CustomEvent`, `DecompressionStream`, `Event`, `EventTarget`, `File`, `FormData`, `Headers`, `PerformanceEntry`, `PerformanceMark`, `PerformanceMeasure`, `PerformanceObserver`, `PerformanceObserverEntryList`, `PerformanceResourceTiming`, `ReadableByteStreamController`, `ReadableStream`, `ReadableStreamBYOBReader`, `ReadableStreamBYOBRequest`, `ReadableStreamDefaultController`, `ReadableStreamDefaultReader`, `Response`, `Request`, `SubtleCrypto`, `DOMException`, `TextDecoder`, `TextDecoderStream`, `TextEncoder`, `TextEncoderStream`, `TransformStream`, `TransformStreamDefaultController`, `URL`, `URLSearchParams`, `WebSocket`, `WritableStream`, `WritableStreamDefaultController`, `WritableStreamDefaultWriter`, `atob`, `btoa`, `clearImmediate`, `clearInterval`, `clearTimeout`, `queueMicrotask`, `require`, `setImmediate`, `setInterval`, `setTimeout`, `structuredClone`. Removed fake/unimplemented globals (`BroadcastChannel`, `EventSource`, `MessageChannel`, `MessageEvent`, `MessagePort`, `Navigator`, `Storage`, `WebAssembly`). | -| **WHATWG FormData** | ✅ Full Core API | WHATWG standard `FormData` implementation with `append`, `delete`, `get`, `getAll`, `has`, `set`, `forEach`, `entries`, `keys`, `values`, `[Symbol.iterator]()`, Blob-to-File normalization with filename override, and `Response.prototype.formData()` multipart/form-data and urlencoded parser. Seamlessly available globally (`new FormData()`). | +| **WHATWG FormData** | ✅ Full Core API | WHATWG standard `FormData` implementation with `append`, `delete`, `get`, `getAll`, `has`, `set`, `forEach`, `entries`, `keys`, `values`, `[Symbol.iterator]()` returning genuine `FormDataIterator` conforming to ECMAScript `IterableIterator` protocol, Blob-to-File normalization with filename override, and `Response.prototype.formData()` multipart/form-data and urlencoded parser. Seamlessly available globally (`new FormData()`). | | **Virtual Machine & Sandbox (`node:vm`)** | ⚠️ Static Type Stubs / Dynamic Tier | Full TypeScript definitions and contract stubs for static compilation; dynamic runtime execution (`runInContext`, `compileFunction`, `evaluate`) deferred to `--dynamic` tier (QuickJS-ng). | | **Readline CLI (`node:readline` & `node:readline/promises`)** | ✅ Full (30 / 30 APIs; 100.0%) | Full core subset parity: `Interface`, `createInterface`, `InterfaceConstructor`, `clearLine`, `clearScreenDown`, `cursorTo`, `moveCursor`, `emitKeypressEvents`, `[Symbol.asyncIterator]`, `[Symbol.dispose]`, promise-based `node:readline/promises` (`Interface`, `createInterface`, `Readline`), line buffering, keypress decoding, prompt, question, cursor management, and stream abort signals. | | **DNS Resolution (`node:dns`)** | ✅ Core APIs | C runtime getaddrinfo / resolv bindings: `lookup`, `lookupService`, `resolve`, `resolve4`, `resolve6`, `resolveCname`, `resolveNs`, `resolvePtr`, `reverse`, `getDefaultResultOrder`, `setDefaultResultOrder`, `promises.*`. Hardcoded mock record resolvers (MX/TXT/SRV/SOA/CAA/NAPTR/TLSA) and dummy in-memory `Resolver`/`getServers`/`setServers` removed. | diff --git a/internal/lowering/builtins_array.go b/internal/lowering/builtins_array.go index c078cdf..ae1099e 100644 --- a/internal/lowering/builtins_array.go +++ b/internal/lowering/builtins_array.go @@ -82,32 +82,289 @@ func registerArrayBuiltins(m map[string]BuiltinIntrinsic) { MinArgs: 1, MaxArgs: 3, Lower: func(call IntrinsicCall, intrinsic BuiltinIntrinsic) (string, ir.Type, error) { - var args []string - retType := ir.TypeNumberArray - for i, argExpr := range call.Expression.Arguments { - argVal, aType, err := call.LowerExpression(call.Path, argExpr, "", call.Function, call.Env, call.Counter, call.Shapes, call.Signatures) - if err != nil { - return "", "", err + argVal, aType, err := call.LowerExpression(call.Path, call.Expression.Arguments[0], "", call.Function, call.Env, call.Counter, call.Shapes, call.Signatures) + if err != nil { + return "", "", err + } + + if isSetType(aType) { + elemType := ir.TypeUnknown + if after, ok := strings.CutPrefix(string(aType), "object:Set__"); ok { + elemType = toIRType(after) + } else if call.Expression.Arguments[0] != nil && strings.Contains(call.Expression.Arguments[0].InferredType, "<") { + inferred := strings.TrimSpace(call.Expression.Arguments[0].InferredType) + idx := strings.Index(inferred, "<") + inner := inferred[idx+1 : len(inferred)-1] + parts := splitTypeArguments(inner) + if len(parts) >= 1 { + elemType = toIRType(parts[0]) + } } - args = append(args, argVal) - if i == 0 { - if strings.HasSuffix(string(aType), "[]") || aType == ir.TypeNumberArray || aType == ir.TypeStringArray || aType == ir.TypeBoolArray || aType == ir.TypeBigIntArray { - retType = aType - } else if aType == ir.TypeString { - retType = ir.TypeStringArray + valuesRes := nextTemp(call.Counter) + valuesArrType := ir.Type(string(elemType) + "[]") + call.Function.Body = append(call.Function.Body, ir.Instruction{ + Op: ir.OpCall, + Type: valuesArrType, + Result: valuesRes, + Callee: "__set.values", + Args: []string{argVal}, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + call.Env[valuesRes] = valuesArrType + argVal = valuesRes + aType = valuesArrType + } else if isMapType(aType) { + keyType := ir.TypeString + valType := ir.TypeUnknown + if after, ok := strings.CutPrefix(string(aType), "object:Map__"); ok { + parts := strings.Split(after, "_") + if len(parts) >= 2 { + keyType = toIRType(parts[0]) + valType = toIRType(strings.Join(parts[1:], "_")) + } + } + var fields []ir.Field + fields = append(fields, ir.Field{Name: "0", Type: keyType}) + fields = append(fields, ir.Field{Name: "1", Type: valType}) + entryShapeName := anonymousShapeName(fields) + registerAnonymousShape(entryShapeName, fields) + elemType := ir.Type("object:" + entryShapeName) + entriesRes := nextTemp(call.Counter) + entriesArrType := ir.Type(string(elemType) + "[]") + call.Function.Body = append(call.Function.Body, ir.Instruction{ + Op: ir.OpCall, + Type: entriesArrType, + Result: entriesRes, + Callee: "__map.entries", + Args: []string{argVal}, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + call.Env[entriesRes] = entriesArrType + argVal = entriesRes + aType = entriesArrType + } + + shapeName := strings.TrimPrefix(string(aType), "object:") + isIter := false + if strings.HasPrefix(string(aType), "object:") { + if _, _, ok := findMethodInHierarchy(shapeName, "next", call.Signatures, classHierarchy); ok { + isIter = true + } else if _, _, ok := findMethodInHierarchy(shapeName, "Symbol.iterator", call.Signatures, classHierarchy); ok { + isIter = true + } + } + if !isIter && (strings.Contains(string(aType), "Generator") || strings.Contains(string(aType), "Iterator")) && !strings.Contains(string(aType), "MapIterator") && !strings.Contains(string(aType), "SetIterator") { + isIter = true + } + + if isIter { + if fnIter, mangledIter, okIter := findMethodInHierarchy(shapeName, "Symbol.iterator", call.Signatures, classHierarchy); okIter { + iterRes := nextTemp(call.Counter) + call.Function.Body = append(call.Function.Body, ir.Instruction{ + Op: ir.OpCall, + Type: fnIter.ReturnType, + Result: iterRes, + Callee: mangledIter, + Args: []string{argVal}, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + argVal = iterRes + aType = fnIter.ReturnType + shapeName = strings.TrimPrefix(string(aType), "object:") + } + + nextFn := shapeName + "_next" + targetNext, hasNext := call.Signatures[nextFn] + if !hasNext { + if fn, mangled, ok := findMethodInHierarchy(shapeName, "next", call.Signatures, classHierarchy); ok { + targetNext = fn + nextFn = mangled + hasNext = true + } + } + + valType := ir.TypeNumber + resShapeName := "" + doneFieldIndex := 0 + valFieldIndex := 1 + if hasNext { + resShapeName = strings.TrimPrefix(string(targetNext.ReturnType), "object:") + if resShape, ok := call.Shapes[resShapeName]; ok { + for idx, f := range resShape.Fields { + if f.Name == "done" { + doneFieldIndex = idx + } else if f.Name == "value" { + valFieldIndex = idx + valType = f.Type + } + } + } + } + if (valType == ir.TypeNumber || valType == "") && call.Expression.Arguments[0] != nil { + inferred := strings.TrimSpace(call.Expression.Arguments[0].InferredType) + if idx := strings.Index(inferred, "<"); idx >= 0 && strings.HasSuffix(inferred, ">") { + inner := inferred[idx+1 : len(inferred)-1] + parts := splitTypeArguments(inner) + if len(parts) > 0 { + valType = toIRType(parts[0]) + } + } else if idx := strings.Index(string(aType), "<"); idx >= 0 && strings.HasSuffix(string(aType), ">") { + inner := string(aType)[idx+1 : len(string(aType))-1] + parts := splitTypeArguments(inner) + if len(parts) > 0 { + valType = toIRType(parts[0]) + } + } + } + if valType == "" { + valType = ir.TypeUnknown + } + retType := ir.Type(string(valType) + "[]") + if valType == ir.TypeString { + retType = ir.TypeStringArray + } else if valType == ir.TypeNumber { + retType = ir.TypeNumberArray + } else if valType == ir.TypeBool { + retType = ir.TypeBoolArray + } else if valType == ir.TypeBigInt { + retType = ir.TypeBigIntArray + } + + arrRes := call.Result + if arrRes == "" { + arrRes = nextTemp(call.Counter) + } + call.Function.Body = append(call.Function.Body, ir.Instruction{ + Op: ir.OpArray, + Type: retType, + Result: arrRes, + Args: nil, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + call.Env[arrRes] = retType + + var mapVal string + if len(call.Expression.Arguments) >= 2 { + mv, _, err := call.LowerExpression(call.Path, call.Expression.Arguments[1], "", call.Function, call.Env, call.Counter, call.Shapes, call.Signatures) + if err != nil { + return "", "", err } + mapVal = mv + } + + condFunc := ir.Function{Name: "cond", ReturnType: ir.TypeBool} + condConst := nextTemp(call.Counter) + condFunc.Body = append(condFunc.Body, ir.Instruction{ + Op: ir.OpConst, Type: ir.TypeBool, Result: condConst, Value: "true", Span: toIRSpan(call.Path, call.Expression.Span), + }) + + bodyBranch := ir.Function{Name: "body", ReturnType: call.Function.ReturnType} + resVal := nextTemp(call.Counter) + retNextType := ir.Type("object:" + resShapeName) + if hasNext { + retNextType = targetNext.ReturnType } + bodyBranch.Body = append(bodyBranch.Body, ir.Instruction{ + Op: ir.OpCall, + Type: retNextType, + Result: resVal, + Callee: nextFn, + Args: []string{argVal}, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + + doneVal := nextTemp(call.Counter) + bodyBranch.Body = append(bodyBranch.Body, ir.Instruction{ + Op: ir.OpFieldGet, + Type: ir.TypeBool, + Result: doneVal, + Callee: resShapeName, + Field: "done", + FieldIndex: doneFieldIndex, + Args: []string{resVal}, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + + bodyBranch.Body = append(bodyBranch.Body, ir.Instruction{ + Op: ir.OpIf, + Type: ir.TypeVoid, + Args: []string{doneVal}, + Then: []ir.Instruction{ + {Op: ir.OpBreak, Type: ir.TypeVoid, Span: toIRSpan(call.Path, call.Expression.Span)}, + }, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + + valVal := nextTemp(call.Counter) + bodyBranch.Body = append(bodyBranch.Body, ir.Instruction{ + Op: ir.OpFieldGet, + Type: valType, + Result: valVal, + Callee: resShapeName, + Field: "value", + FieldIndex: valFieldIndex, + Args: []string{resVal}, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + + pushVal := valVal + if mapVal != "" { + mappedVal := nextTemp(call.Counter) + bodyBranch.Body = append(bodyBranch.Body, ir.Instruction{ + Op: ir.OpCall, + Type: valType, + Result: mappedVal, + Callee: mapVal, + Args: []string{valVal}, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + pushVal = mappedVal + } + + pushRes := nextTemp(call.Counter) + bodyBranch.Body = append(bodyBranch.Body, ir.Instruction{ + Op: ir.OpCall, + Type: ir.TypeNumber, + Result: pushRes, + Callee: "__array.push", + Args: []string{arrRes, pushVal}, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + + call.Function.Body = append(call.Function.Body, ir.Instruction{ + Op: ir.OpWhile, + Type: ir.TypeVoid, + Args: []string{condConst}, + Cond: condFunc.Body, + Body: bodyBranch.Body, + Span: toIRSpan(call.Path, call.Expression.Span), + }) + + return arrRes, retType, nil + } + + // Fast-path: Array or String slice + retType := ir.TypeNumberArray + if strings.HasSuffix(string(aType), "[]") || aType == ir.TypeNumberArray || aType == ir.TypeStringArray || aType == ir.TypeBoolArray || aType == ir.TypeBigIntArray { + retType = aType + } else if aType == ir.TypeString { + retType = ir.TypeStringArray } - if len(args) >= 2 { + + if len(call.Expression.Arguments) >= 2 { rawArr := nextTemp(call.Counter) call.Function.Body = append(call.Function.Body, ir.Instruction{ Op: ir.OpCall, Type: retType, Result: rawArr, Callee: "__array.from", - Args: []string{args[0]}, + Args: []string{argVal}, Span: toIRSpan(call.Path, call.Expression.Span), }) + mapVal, _, err := call.LowerExpression(call.Path, call.Expression.Arguments[1], "", call.Function, call.Env, call.Counter, call.Shapes, call.Signatures) + if err != nil { + return "", "", err + } result := call.Result if result == "" { result = nextTemp(call.Counter) @@ -117,11 +374,12 @@ func registerArrayBuiltins(m map[string]BuiltinIntrinsic) { Type: retType, Result: result, Callee: "__array.map", - Args: []string{rawArr, args[1]}, + Args: []string{rawArr, mapVal}, Span: toIRSpan(call.Path, call.Expression.Span), }) return result, retType, nil } + result := call.Result if result == "" { result = nextTemp(call.Counter) @@ -131,7 +389,7 @@ func registerArrayBuiltins(m map[string]BuiltinIntrinsic) { Type: retType, Result: result, Callee: "__array.from", - Args: args, + Args: []string{argVal}, Span: toIRSpan(call.Path, call.Expression.Span), }) return result, retType, nil diff --git a/internal/lowering/generics.go b/internal/lowering/generics.go index 9a41e8a..63a658f 100644 --- a/internal/lowering/generics.go +++ b/internal/lowering/generics.go @@ -20,6 +20,7 @@ var ( // SpecializeGenerics monomorphizes generic functions and classes based on // concrete type arguments at call sites, instantiations, and type annotations. func SpecializeGenerics(program frontend.Program) (frontend.Program, error) { + initializeTypeAliases(program) initializeClassIdentities(program) buildClassHierarchy(program) genericFuncs := map[string]typescriptgo.SyntaxStatement{} @@ -444,6 +445,7 @@ func SpecializeGenerics(program frontend.Program) (frontend.Program, error) { } specCls.Methods = concreteMethods + kind := genericClassKinds[name] if kind == "" { kind = "class" diff --git a/internal/lowering/generics_subst.go b/internal/lowering/generics_subst.go index 06c9773..fc33912 100644 --- a/internal/lowering/generics_subst.go +++ b/internal/lowering/generics_subst.go @@ -4,6 +4,7 @@ import ( "strings" typescriptgo "github.com/microsoft/TypeScript/tsc/scriptgo" + "github.com/pilotworks/scriptgo/internal/ir" ) func isBuiltinGeneric(name string) bool { @@ -60,16 +61,34 @@ func mangleGenericName(name string, typeArgs []string) string { func splitTypeArguments(s string) []string { var res []string - depth := 0 + angleDepth := 0 + bracketDepth := 0 + braceDepth := 0 + parenDepth := 0 start := 0 for i, r := range s { - if r == '<' { - depth++ - } else if r == '>' { - depth-- - } else if r == ',' && depth == 0 { - res = append(res, strings.TrimSpace(s[start:i])) - start = i + 1 + switch r { + case '<': + angleDepth++ + case '>': + angleDepth-- + case '[': + bracketDepth++ + case ']': + bracketDepth-- + case '{': + braceDepth++ + case '}': + braceDepth-- + case '(': + parenDepth++ + case ')': + parenDepth-- + case ',': + if angleDepth == 0 && bracketDepth == 0 && braceDepth == 0 && parenDepth == 0 { + res = append(res, strings.TrimSpace(s[start:i])) + start = i + 1 + } } } if start < len(s) { @@ -137,8 +156,8 @@ func substituteType(typ string, subst map[string]string) string { return res } } - if strings.Contains(clean, "|") { - parts := strings.Split(clean, "|") + parts := splitTopLevelUnion(clean) + if len(parts) > 1 { var newParts []string for _, p := range parts { newParts = append(newParts, substituteType(strings.TrimSpace(p), subst)) @@ -189,7 +208,74 @@ func substituteType(typ string, subst map[string]string) string { } return res } + if strings.HasPrefix(clean, "{") && strings.HasSuffix(clean, "}") { + inner := strings.TrimSpace(clean[1 : len(clean)-1]) + var parts []string + start := 0 + depth := 0 + for i, r := range inner { + switch r { + case '<', '(', '[', '{': + depth++ + case '>', ')', ']', '}': + depth-- + case ';', ',': + if depth == 0 { + p := strings.TrimSpace(inner[start:i]) + if p != "" { + parts = append(parts, p) + } + start = i + 1 + } + } + } + if start < len(inner) { + p := strings.TrimSpace(inner[start:]) + if p != "" { + parts = append(parts, p) + } + } + var newFields []string + for _, part := range parts { + colonIdx := strings.Index(part, ":") + if colonIdx != -1 { + name := strings.TrimSpace(part[:colonIdx]) + fType := strings.TrimSpace(part[colonIdx+1:]) + substFType := substituteType(fType, subst) + newFields = append(newFields, name+": "+substFType) + } else { + newFields = append(newFields, part) + } + } + res := "{ " + strings.Join(newFields, "; ") + " }" + if hasObj { + return "object:" + res + } + return res + } if strings.HasPrefix(clean, "__shape_") { + var shapeFields []ir.Field + if s, ok := anonymousShapes[clean]; ok && len(s.Fields) > 0 { + shapeFields = s.Fields + } else if s, ok := registeredShapes[clean]; ok && len(s.Fields) > 0 { + shapeFields = s.Fields + } + if len(shapeFields) > 0 { + var newFields []ir.Field + for _, f := range shapeFields { + sub := substituteType(string(f.Type), subst) + newFields = append(newFields, ir.Field{ + Name: f.Name, + Type: toIRType(sub), + }) + } + name := anonymousShapeName(newFields) + registerAnonymousShape(name, newFields) + if hasObj { + return "object:" + name + } + return name + } cleanShape := strings.TrimPrefix(clean, "__shape_") tokens := strings.Split(cleanShape, "_") var newTokens []string @@ -242,7 +328,7 @@ func substituteType(typ string, subst map[string]string) string { } if val, ok := subst[clean]; ok { if hasObj { - return "object:" + val + return string(toIRType(val)) } return val } diff --git a/internal/lowering/statements_control.go b/internal/lowering/statements_control.go index 2e2dddd..ddc9071 100644 --- a/internal/lowering/statements_control.go +++ b/internal/lowering/statements_control.go @@ -209,35 +209,93 @@ func lowerForOf(path string, statement typescriptgo.SyntaxStatement, function *i // Intl.Segments is iterable but is not an array-backed value. arrType = ir.Type("Intl.Segment[]") } - if (strings.Contains(string(arrType), "Generator") || strings.Contains(string(arrType), "Iterator")) && !strings.Contains(string(arrType), "MapIterator") && !strings.Contains(string(arrType), "SetIterator") { - shapeName := strings.TrimPrefix(string(arrType), "object:") + isIteratorOrIterable := false + shapeName := "" + if strings.HasPrefix(string(arrType), "object:") { + shapeName = strings.TrimPrefix(string(arrType), "object:") + if _, _, ok := findMethodInHierarchy(shapeName, "next", signatures, classHierarchy); ok { + isIteratorOrIterable = true + } else if _, _, ok := findMethodInHierarchy(shapeName, "Symbol.iterator", signatures, classHierarchy); ok { + isIteratorOrIterable = true + } + } + if !isIteratorOrIterable && (strings.Contains(string(arrType), "Generator") || strings.Contains(string(arrType), "Iterator")) && !strings.Contains(string(arrType), "MapIterator") && !strings.Contains(string(arrType), "SetIterator") { + isIteratorOrIterable = true + shapeName = strings.TrimPrefix(string(arrType), "object:") + } + + if isIteratorOrIterable { + if fnIter, mangledIter, okIter := findMethodInHierarchy(shapeName, "Symbol.iterator", signatures, classHierarchy); okIter { + iterRes := nextTemp(counter) + function.Body = append(function.Body, ir.Instruction{ + Op: ir.OpCall, + Type: fnIter.ReturnType, + Result: iterRes, + Callee: mangledIter, + Args: []string{arrVal}, + Span: toIRSpan(path, statement.Span), + }) + arrVal = iterRes + arrType = fnIter.ReturnType + shapeName = strings.TrimPrefix(string(arrType), "object:") + } + nextFn := shapeName + "_next" targetNext, hasNext := signatures[nextFn] + if !hasNext { + if fn, mangled, ok := findMethodInHierarchy(shapeName, "next", signatures, classHierarchy); ok { + targetNext = fn + nextFn = mangled + hasNext = true + } + } valType := ir.TypeNumber resShapeName := "" + doneFieldIndex := 0 + valFieldIndex := 1 if hasNext { resShapeName = strings.TrimPrefix(string(targetNext.ReturnType), "object:") - if resShape, ok := shapes[resShapeName]; ok && len(resShape.Fields) > 1 { - valType = resShape.Fields[1].Type + if resShape, ok := shapes[resShapeName]; ok { + for idx, f := range resShape.Fields { + if f.Name == "done" { + doneFieldIndex = idx + } else if f.Name == "value" { + valFieldIndex = idx + valType = f.Type + } + } } } else { - if statement.Expression != nil && strings.Contains(statement.Expression.InferredType, "<") && strings.HasSuffix(strings.TrimSpace(statement.Expression.InferredType), ">") { - inferred := strings.TrimSpace(statement.Expression.InferredType) - idx := strings.Index(inferred, "<") - inner := inferred[idx+1 : len(inferred)-1] - parts := splitTypeArguments(inner) - if len(parts) > 0 { - valType = toIRType(parts[0]) - } - } else if strings.Contains(string(arrType), "<") && strings.HasSuffix(string(arrType), ">") { - idx := strings.Index(string(arrType), "<") - inner := string(arrType)[idx+1 : len(string(arrType))-1] - parts := splitTypeArguments(inner) - if len(parts) > 0 { - valType = toIRType(parts[0]) + nextFn = "__generator.next" + resShapeName = fmt.Sprintf("IteratorResult_%s", valType) + } + + typeArg := "" + if statement.Expression != nil && strings.Contains(statement.Expression.InferredType, "<") && strings.HasSuffix(strings.TrimSpace(statement.Expression.InferredType), ">") { + inferred := strings.TrimSpace(statement.Expression.InferredType) + idx := strings.Index(inferred, "<") + inner := inferred[idx+1 : len(inferred)-1] + parts := splitTypeArguments(inner) + if len(parts) > 0 { + typeArg = parts[0] + } + } else if strings.Contains(string(arrType), "<") && strings.HasSuffix(string(arrType), ">") { + idx := strings.Index(string(arrType), "<") + inner := string(arrType)[idx+1 : len(string(arrType))-1] + parts := splitTypeArguments(inner) + if len(parts) > 0 { + typeArg = parts[0] + } + } + if typeArg != "" { + if specialized := toIRType(typeArg); specialized != "" { + if valType == "object:T" || valType == "T" || strings.HasPrefix(string(valType), "object:T") || valType == ir.TypeUnknown || valType == ir.TypeNumber { + valType = specialized } } - nextFn = "__generator.next" + } + + if !hasNext { resShapeName = fmt.Sprintf("IteratorResult_%s", valType) if _, exists := shapes[resShapeName]; !exists { shapes[resShapeName] = ir.ObjectShape{ @@ -251,6 +309,7 @@ func lowerForOf(path string, statement typescriptgo.SyntaxStatement, function *i } } + condFunc := ir.Function{Name: "cond", ReturnType: ir.TypeBool} condConst := nextTemp(counter) condFunc.Body = append(condFunc.Body, ir.Instruction{Op: ir.OpConst, Type: ir.TypeBool, Result: condConst, Value: "true", Span: toIRSpan(path, statement.Span)}) @@ -280,7 +339,7 @@ func lowerForOf(path string, statement typescriptgo.SyntaxStatement, function *i Result: doneVal, Callee: resShapeName, Field: "done", - FieldIndex: 0, + FieldIndex: doneFieldIndex, Args: []string{resVal}, Span: toIRSpan(path, statement.Span), }) @@ -302,7 +361,7 @@ func lowerForOf(path string, statement typescriptgo.SyntaxStatement, function *i Result: valVal, Callee: resShapeName, Field: "value", - FieldIndex: 1, + FieldIndex: valFieldIndex, Args: []string{resVal}, Span: toIRSpan(path, statement.Span), }) diff --git a/internal/lowering/type_aliases.go b/internal/lowering/type_aliases.go new file mode 100644 index 0000000..5235490 --- /dev/null +++ b/internal/lowering/type_aliases.go @@ -0,0 +1,37 @@ +package lowering + +import ( + "strings" + + "github.com/pilotworks/scriptgo/internal/frontend" +) + +// initializeTypeAliases builds the type alias and enum index across all files +// in the program (including declaration files) so that monomorphization and +// lowering have access to spec-defined and user-defined type aliases. +func initializeTypeAliases(program frontend.Program) { + if typeAliasesIndex == nil { + typeAliasesIndex = make(map[string]string) + } + for _, file := range program.Files { + for _, statement := range file.Syntax.Statements { + if statement.Kind == "type_alias" && statement.Name != "" && statement.Type != "" { + typeAliasesIndex[statement.Name] = statement.Type + } + if statement.Kind == "enum" && statement.Enum != nil { + hasString := false + for _, m := range statement.Enum.Members { + if m.Value != "" && (strings.HasPrefix(m.Value, "\"") || strings.HasPrefix(m.Value, "'")) { + hasString = true + break + } + } + if hasString { + typeAliasesIndex[statement.Enum.Name] = "string" + } else { + typeAliasesIndex[statement.Enum.Name] = "number" + } + } + } + } +} diff --git a/internal/typescriptgo/stdlib/formdata.ts b/internal/typescriptgo/stdlib/formdata.ts index 2f8a974..d3ef839 100644 --- a/internal/typescriptgo/stdlib/formdata.ts +++ b/internal/typescriptgo/stdlib/formdata.ts @@ -12,15 +12,16 @@ export class FormDataEntry { } } -class FormDataIterator { - private _values: unknown[]; +export class FormDataIterator { + private _values: T[]; private _index: number = 0; - constructor(values: unknown[]) { + constructor(values: T[]) { this._values = values; + this._index = 0; } - next(): { value: unknown; done: boolean } { + next(): { value: T | undefined; done: boolean } { if (this._index < this._values.length) { const value = this._values[this._index]; this._index = this._index + 1; @@ -29,7 +30,7 @@ class FormDataIterator { return { value: undefined, done: true }; } - [Symbol.iterator](): FormDataIterator { + [Symbol.iterator](): FormDataIterator { return this; } } @@ -114,32 +115,32 @@ export class FormData { } } - entries(): [string, FormDataEntryValue][] { + entries(): FormDataIterator<[string, FormDataEntryValue]> { const res: [string, FormDataEntryValue][] = []; for (let i = 0; i < this._entries.length; i++) { res.push([this._entries[i].name, this._entries[i].value]); } - return res; + return new FormDataIterator<[string, FormDataEntryValue]>(res); } - keys(): string[] { + keys(): FormDataIterator { const res: string[] = []; for (let i = 0; i < this._entries.length; i++) { res.push(this._entries[i].name); } - return res; + return new FormDataIterator(res); } - values(): FormDataEntryValue[] { + values(): FormDataIterator { const res: FormDataEntryValue[] = []; for (let i = 0; i < this._entries.length; i++) { res.push(this._entries[i].value); } - return res; + return new FormDataIterator(res); } - [Symbol.iterator](): FormDataIterator { - return new FormDataIterator(this.entries()); + [Symbol.iterator](): FormDataIterator<[string, FormDataEntryValue]> { + return this.entries(); } private _toEntryValue(value: unknown, fileName?: string): FormDataEntryValue { diff --git a/internal/typescriptgo/stdlib/globals.d.ts b/internal/typescriptgo/stdlib/globals.d.ts index c3f5849..ccc66f7 100644 --- a/internal/typescriptgo/stdlib/globals.d.ts +++ b/internal/typescriptgo/stdlib/globals.d.ts @@ -779,6 +779,11 @@ declare function clearInterval(id: number | undefined): void; declare function setImmediate(callback: (...args: unknown[]) => void, ...args: unknown[]): number; declare function clearImmediate(id: number | undefined): void; +interface URLSearchParamsIterator extends IterableIterator { + next(): IteratorResult; + [Symbol.iterator](): URLSearchParamsIterator; +} + interface URLSearchParams { append(name: string, value: string): void; delete(name: string): void; @@ -789,6 +794,10 @@ interface URLSearchParams { sort(): void; toString(): string; readonly size: number; + entries(): URLSearchParamsIterator<[string, string]>; + keys(): URLSearchParamsIterator; + values(): URLSearchParamsIterator; + [Symbol.iterator](): URLSearchParamsIterator<[string, string]>; } interface URLSearchParamsConstructor { @@ -823,6 +832,11 @@ interface URLConstructor { declare var URL: URLConstructor; +interface HeadersIterator extends IterableIterator { + next(): IteratorResult; + [Symbol.iterator](): HeadersIterator; +} + interface Headers { append(name: string, value: string): void; delete(name: string): void; @@ -830,9 +844,10 @@ interface Headers { has(name: string): boolean; set(name: string, value: string): void; forEach(callback: (value: string, name: string, parent: Headers) => void): void; - entries(): [string, string][]; - keys(): string[]; - values(): string[]; + entries(): HeadersIterator<[string, string]>; + keys(): HeadersIterator; + values(): HeadersIterator; + [Symbol.iterator](): HeadersIterator<[string, string]>; } interface HeadersConstructor { @@ -844,6 +859,11 @@ declare var Headers: HeadersConstructor; type FormDataEntryValue = File | string; +interface FormDataIterator extends IterableIterator { + next(): IteratorResult; + [Symbol.iterator](): FormDataIterator; +} + interface FormData { append(name: string, value: string | Blob, fileName?: string): void; delete(name: string): void; @@ -852,10 +872,10 @@ interface FormData { has(name: string): boolean; set(name: string, value: string | Blob, fileName?: string): void; forEach(callback: (value: FormDataEntryValue, key: string, parent: FormData) => void): void; - entries(): [string, FormDataEntryValue][]; - keys(): string[]; - values(): FormDataEntryValue[]; - [Symbol.iterator](): IteratorObject<[string, FormDataEntryValue]>; + entries(): FormDataIterator<[string, FormDataEntryValue]>; + keys(): FormDataIterator; + values(): FormDataIterator; + [Symbol.iterator](): FormDataIterator<[string, FormDataEntryValue]>; } interface FormDataConstructor { @@ -974,6 +994,11 @@ interface IteratorResult { value: T; } +interface IterableIterator { + next(...args: [] | [TNext]): IteratorResult; + [Symbol.iterator](): IterableIterator; +} + interface IteratorObject { [Symbol.iterator](): IteratorObject; next(...args: [] | [TNext]): IteratorResult; diff --git a/internal/typescriptgo/stdlib/http.ts b/internal/typescriptgo/stdlib/http.ts index fb25c0f..341ba67 100644 --- a/internal/typescriptgo/stdlib/http.ts +++ b/internal/typescriptgo/stdlib/http.ts @@ -9,6 +9,29 @@ import { URL } from "node:url"; import { FormData } from "node:formdata"; import { File } from "node:buffer"; +export class HeadersIterator { + private _values: T[]; + private _index: number = 0; + + constructor(values: T[]) { + this._values = values; + this._index = 0; + } + + next(): { value: T | undefined; done: boolean } { + if (this._index < this._values.length) { + const value = this._values[this._index]; + this._index = this._index + 1; + return { value: value, done: false }; + } + return { value: undefined, done: true }; + } + + [Symbol.iterator](): HeadersIterator { + return this; + } +} + export class Headers { _keys: string[] = []; _values: string[] = []; @@ -18,9 +41,9 @@ export class Headers { this._values = []; if (init !== null && init !== undefined) { if (init instanceof Headers) { - const entries = (init as Headers).entries(); - for (let i = 0; i < entries.length; i++) { - this.append(entries[i][0], entries[i][1]); + const other = init as Headers; + for (let i = 0; i < other._keys.length; i++) { + this.append(other._keys[i], other._values[i]); } } else if (Array.isArray(init)) { for (let i = 0; i < init.length; i++) { @@ -103,21 +126,25 @@ export class Headers { } } - entries(): string[][] { - const res: string[][] = []; + entries(): HeadersIterator<[string, string]> { + const res: [string, string][] = []; for (let i = 0; i < this._keys.length; i++) { - const pair: string[] = [this._keys[i], this._values[i]]; + const pair: [string, string] = [this._keys[i], this._values[i]]; res.push(pair); } - return res; + return new HeadersIterator<[string, string]>(res); + } + + keys(): HeadersIterator { + return new HeadersIterator(this._keys); } - keys(): string[] { - return this._keys; + values(): HeadersIterator { + return new HeadersIterator(this._values); } - values(): string[] { - return this._values; + [Symbol.iterator](): HeadersIterator<[string, string]> { + return this.entries(); } } @@ -409,11 +436,10 @@ export async function fetch(input: unknown, init: RequestInit = defaultRequestIn if (init.headers instanceof Headers) { headers = init.headers as Headers; } - const headerEntries = headers.entries(); const flatHeaders: string[] = []; - for (let i = 0; i < headerEntries.length; i++) { - flatHeaders.push(headerEntries[i][0]); - flatHeaders.push(headerEntries[i][1]); + for (let i = 0; i < headers._keys.length; i++) { + flatHeaders.push(headers._keys[i]); + flatHeaders.push(headers._values[i]); } const raw = __scriptgo.fetchSync(url, method, flatHeaders, body); const respHeaders = new Headers(); diff --git a/internal/typescriptgo/stdlib/url.ts b/internal/typescriptgo/stdlib/url.ts index 577cd05..4834b6e 100644 --- a/internal/typescriptgo/stdlib/url.ts +++ b/internal/typescriptgo/stdlib/url.ts @@ -8,18 +8,16 @@ class URLSearchParamEntry { } } -class URLSearchParamsIterator { - // Kept optional for compatibility with code that probes array-like values; - // real iterators do not expose a length property. - length?: number; - private _values: unknown[]; +export class URLSearchParamsIterator { + private _values: T[]; private _index: number = 0; - constructor(values: unknown[]) { + constructor(values: T[]) { this._values = values; + this._index = 0; } - next(): { value: unknown; done: boolean } { + next(): { value: T | undefined; done: boolean } { if (this._index < this._values.length) { const value = this._values[this._index]; this._index = this._index + 1; @@ -28,7 +26,7 @@ class URLSearchParamsIterator { return { value: undefined, done: true }; } - [Symbol.iterator](): URLSearchParamsIterator { + [Symbol.iterator](): URLSearchParamsIterator { return this; } } @@ -149,28 +147,32 @@ export class URLSearchParams { } } - entries(): [string, string][] { + entries(): URLSearchParamsIterator<[string, string]> { const res: [string, string][] = []; for (let i = 0; i < this._entries.length; i++) { res.push([this._entries[i].name, this._entries[i].value]); } - return res; + return new URLSearchParamsIterator<[string, string]>(res); } - keys(): string[] { + keys(): URLSearchParamsIterator { const res: string[] = []; for (let i = 0; i < this._entries.length; i++) { res.push(this._entries[i].name); } - return res; + return new URLSearchParamsIterator(res); } - values(): string[] { + values(): URLSearchParamsIterator { const res: string[] = []; for (let i = 0; i < this._entries.length; i++) { res.push(this._entries[i].value); } - return res; + return new URLSearchParamsIterator(res); + } + + [Symbol.iterator](): URLSearchParamsIterator<[string, string]> { + return this.entries(); } forEach(fn: (value: string, name: string, parent: URLSearchParams) => void, thisArg: unknown = null): void { From a335169b4ebe51d93970710983a583419adb8ee8 Mon Sep 17 00:00:00 2001 From: Pham Tan Minh Tien Date: Thu, 24 Sep 2026 09:31:02 +0700 Subject: [PATCH 2/2] fix(lowering): format go files to satisfy gofmt --- internal/lowering/generics.go | 1 - internal/lowering/statements_control.go | 1 - internal/typescriptgo/parse.go | 1 - 3 files changed, 3 deletions(-) diff --git a/internal/lowering/generics.go b/internal/lowering/generics.go index 63a658f..0eebea8 100644 --- a/internal/lowering/generics.go +++ b/internal/lowering/generics.go @@ -445,7 +445,6 @@ func SpecializeGenerics(program frontend.Program) (frontend.Program, error) { } specCls.Methods = concreteMethods - kind := genericClassKinds[name] if kind == "" { kind = "class" diff --git a/internal/lowering/statements_control.go b/internal/lowering/statements_control.go index ddc9071..8bb339e 100644 --- a/internal/lowering/statements_control.go +++ b/internal/lowering/statements_control.go @@ -309,7 +309,6 @@ func lowerForOf(path string, statement typescriptgo.SyntaxStatement, function *i } } - condFunc := ir.Function{Name: "cond", ReturnType: ir.TypeBool} condConst := nextTemp(counter) condFunc.Body = append(condFunc.Body, ir.Instruction{Op: ir.OpConst, Type: ir.TypeBool, Result: condConst, Value: "true", Span: toIRSpan(path, statement.Span)}) diff --git a/internal/typescriptgo/parse.go b/internal/typescriptgo/parse.go index bf230ca..dd612e1 100644 --- a/internal/typescriptgo/parse.go +++ b/internal/typescriptgo/parse.go @@ -317,4 +317,3 @@ func symbolKind(flags ast.SymbolFlags) string { } return "symbol" } -