From 8d8794a4587fb11c847125e5af92c5e14f8e8683 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 2 Oct 2022 14:08:32 +0100 Subject: [PATCH 1/4] compiler: improve helpers for indentation management. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename fc.Indent() → fc.Indented() for indenting code generated in a callback. - Add fc.Indentation() to generate the indentation sequence itself. (cherry picked from commit f86fde69853d22b3c97dbb64f9b4b9d351b4c946) --- compiler/package.go | 8 ++++---- compiler/statements.go | 10 +++++----- compiler/utils.go | 13 +++++++++++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/compiler/package.go b/compiler/package.go index ba57cfc42..d9b410271 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -913,16 +913,16 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, } if prefix != "" { - bodyOutput = strings.Repeat("\t", c.pkgCtx.indentation+1) + "/* */" + prefix + "\n" + bodyOutput + bodyOutput = c.Indentation(1) + "/* */" + prefix + "\n" + bodyOutput } if suffix != "" { - bodyOutput = bodyOutput + strings.Repeat("\t", c.pkgCtx.indentation+1) + "/* */" + suffix + "\n" + bodyOutput = bodyOutput + c.Indentation(1) + "/* */" + suffix + "\n" } if localVarDefs != "" { - bodyOutput = strings.Repeat("\t", c.pkgCtx.indentation+1) + localVarDefs + bodyOutput + bodyOutput = c.Indentation(1) + localVarDefs + bodyOutput } c.pkgCtx.escapingVars = prevEV - return params, fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(params, ", "), bodyOutput, strings.Repeat("\t", c.pkgCtx.indentation)) + return params, fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(params, ", "), bodyOutput, c.Indentation(0)) } diff --git a/compiler/statements.go b/compiler/statements.go index 225cd9cdb..fa203a5b3 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -100,7 +100,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { data.endCase = fc.caseCounter fc.caseCounter++ - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) }) fc.Printf("case %d:", data.endCase) @@ -112,7 +112,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("%s:", label.Name()) } fc.Printf("switch (0) { default:") - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) }) fc.Printf("}") @@ -614,7 +614,7 @@ func (fc *funcContext) translateBranchingStmt(caseClauses []*ast.CaseClause, def for i, clause := range caseClauses { fc.SetPos(clause.Pos()) fc.PrintCond(!flatten, fmt.Sprintf("%sif (%s) {", prefix, condStrs[i]), fmt.Sprintf("case %d:", caseOffset+i)) - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(clause.Body) if flatten && (i < len(caseClauses)-1 || defaultClause != nil) && !astutil.EndsWithReturn(clause.Body) { fc.Printf("$s = %d; continue;", endCase) @@ -625,7 +625,7 @@ func (fc *funcContext) translateBranchingStmt(caseClauses []*ast.CaseClause, def if defaultClause != nil { fc.PrintCond(!flatten, prefix+"{", fmt.Sprintf("case %d:", caseOffset+len(caseClauses))) - fc.Indent(func() { + fc.Indented(func() { fc.translateStmtList(defaultClause.Body) }) } @@ -655,7 +655,7 @@ func (fc *funcContext) translateLoopingStmt(cond func() string, body *ast.BlockS } isTerminated := false fc.PrintCond(!flatten, "while (true) {", fmt.Sprintf("case %d:", data.beginCase)) - fc.Indent(func() { + fc.Indented(func() { condStr := cond() if condStr != "true" { fc.PrintCond(!flatten, fmt.Sprintf("if (!(%s)) { break; }", condStr), fmt.Sprintf("if(!(%s)) { $s = %d; continue; }", condStr, data.endCase)) diff --git a/compiler/utils.go b/compiler/utils.go index 46794fe64..95c791245 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -38,7 +38,7 @@ func (fc *funcContext) Write(b []byte) (int, error) { } func (fc *funcContext) Printf(format string, values ...interface{}) { - fc.Write([]byte(strings.Repeat("\t", fc.pkgCtx.indentation))) + fc.Write([]byte(fc.Indentation(0))) fmt.Fprintf(fc, format, values...) fc.Write([]byte{'\n'}) fc.Write(fc.delayedOutput) @@ -66,12 +66,21 @@ func (fc *funcContext) writePos() { } } -func (fc *funcContext) Indent(f func()) { +// Indented increases generated code indentation level by 1 for the code emitted +// from the callback f. +func (fc *funcContext) Indented(f func()) { fc.pkgCtx.indentation++ f() fc.pkgCtx.indentation-- } +// Indentation returns a sequence of "\t" characters appropriate to the current +// generated code indentation level. The `extra` parameter provides relative +// indentation adjustment. +func (fc *funcContext) Indentation(extra int) string { + return strings.Repeat("\t", fc.pkgCtx.indentation+extra) +} + func (fc *funcContext) CatchOutput(indent int, f func()) []byte { origoutput := fc.output fc.output = nil From 606129e63df2f7d54ea7369c23333dee2f8ec2d4 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 14 Apr 2024 00:00:20 +0100 Subject: [PATCH 2/4] compiler: readability improvements to the functionContext type. - Document semantics of the `functionContext` fields (to the best of my knowledge, at least). - Move `signatureTypes` to the `typesutil.Signature` and add unit tests. - Make use of the `typesutil.Signature` helper type for more self-explanatory condition checks in the compiler code. (based on commit 161d21d5469ce765f4b3b0e7a35f75a02aa49a32) --- compiler/expressions.go | 7 +- compiler/package.go | 78 +++++++++---- compiler/statements.go | 2 +- compiler/typesutil/signature.go | 67 +++++++++++ compiler/typesutil/signature_test.go | 166 +++++++++++++++++++++++++++ compiler/utils.go | 52 +-------- 6 files changed, 294 insertions(+), 78 deletions(-) create mode 100644 compiler/typesutil/signature.go create mode 100644 compiler/typesutil/signature_test.go diff --git a/compiler/expressions.go b/compiler/expressions.go index b393a93d3..d113a7774 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -873,9 +873,8 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, case *ast.SelectorExpr: isJs = typesutil.IsJsPackage(fc.pkgCtx.Uses[fun.Sel].Pkg()) } - sig := fc.typeOf(expr.Fun).Underlying().(*types.Signature) - sigTypes := signatureTypes{Sig: sig} - args := fc.translateArgs(sig, expr.Args, expr.Ellipsis.IsValid()) + sig := typesutil.Signature{Sig: fc.typeOf(expr.Fun).Underlying().(*types.Signature)} + args := fc.translateArgs(sig.Sig, expr.Args, expr.Ellipsis.IsValid()) if !isBuiltin && !isJs { // Normal function calls don't require wrappers. @@ -897,7 +896,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, // Subtle: the proxy lambda argument needs to be assigned with the type // that the original function expects, and not with the argument // expression result type, or we may do implicit type conversion twice. - callArgs[i] = fc.newIdent(v, sigTypes.Param(i, ellipsis.IsValid())) + callArgs[i] = fc.newIdent(v, sig.Param(i, ellipsis.IsValid())) } wrapper := &ast.CallExpr{ Fun: expr.Fun, diff --git a/compiler/package.go b/compiler/package.go index d9b410271..79230b4a8 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -43,27 +43,61 @@ type pkgContext struct { instanceSet *typeparams.PackageInstanceSets } -// funcContext maintains compiler context for a specific function (lexical scope?). +// funcContext maintains compiler context for a specific function. +// +// An instance of this type roughly corresponds to a lexical scope for generated +// JavaScript code (as defined for `var` declarations). type funcContext struct { *analysis.FuncInfo + // Surrounding package context. pkgCtx *pkgContext + // Function context, surrounding this function definition. For package-level + // functions or methods it is the package-level function context (even though + // it technically doesn't correspond to a function). nil for the package-level + // function context. parent *funcContext - // Signature of the function this context corresponds to or nil. For generic - // functions it is the original generic signature to make sure result variable - // identity in the signature matches the variable objects referenced in the - // function body. - sig *types.Signature - allVars map[string]int - localVars []string - resultNames []ast.Expr - flowDatas map[*types.Label]*flowData - caseCounter int - labelCases map[*types.Label]int - output []byte + // Signature of the function this context corresponds to or nil for the + // package-level function context. For generic functions it is the original + // generic signature to make sure result variable identity in the signature + // matches the variable objects referenced in the function body. + sig *typesutil.Signature + // All variable names available in the current function scope. The key is a Go + // variable name and the value is the number of synonymous variable names + // visible from this scope (e.g. due to shadowing). This number is used to + // avoid conflicts when assigning JS variable names for Go variables. + allVars map[string]int + // Local JS variable names defined within this function context. This list + // contains JS variable names assigned to Go variables, as well as other + // auxiliary variables the compiler needs. It is used to generate `var` + // declaration at the top of the function, as well as context save/restore. + localVars []string + // AST expressions representing function's named return values. nil if the + // function has no return values or they are not named. + resultNames []ast.Expr + // Function's internal control flow graph used for generation of a "flattened" + // version of the function when the function is blocking or uses goto. + // TODO(nevkontakte): Describe the exact semantics of this map. + flowDatas map[*types.Label]*flowData + // Number of control flow blocks in a "flattened" function. + caseCounter int + // A mapping from Go labels statements (e.g. labelled loop) to the flow block + // id corresponding to it. + labelCases map[*types.Label]int + // Generated code buffer for the current function. + output []byte + // Generated code that should be emitted at the end of the JS statement. delayedOutput []byte - posAvailable bool - pos token.Pos - typeResolver *typeparams.Resolver + // Set to true if source position is available and should be emitted for the + // source map. + posAvailable bool + // Current position in the Go source code. + pos token.Pos + // For each instantiation of a generic function or method, contains the + // current mapping between type parameters and corresponding type arguments. + // The mapping is used to determine concrete types for expressions within the + // instance's context. Can be nil outside of the generic context, in which + // case calling its methods is safe and simply does no substitution. + typeResolver *typeparams.Resolver // Mapping from function-level objects to JS variable names they have been assigned. objectNames map[types.Object]string } @@ -779,7 +813,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, labelCases: make(map[*types.Label]int), typeResolver: outerContext.typeResolver, objectNames: map[types.Object]string{}, - sig: sig, + sig: &typesutil.Signature{Sig: sig}, } for k, v := range outerContext.allVars { c.allVars[k] = v @@ -816,10 +850,10 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, c.handleEscapingVars(body) } - if c.sig != nil && c.sig.Results().Len() != 0 && c.sig.Results().At(0).Name() != "" { - c.resultNames = make([]ast.Expr, c.sig.Results().Len()) - for i := 0; i < c.sig.Results().Len(); i++ { - result := c.sig.Results().At(i) + if c.sig != nil && c.sig.HasNamedResults() { + c.resultNames = make([]ast.Expr, c.sig.Sig.Results().Len()) + for i := 0; i < c.sig.Sig.Results().Len(); i++ { + result := c.sig.Sig.Results().At(i) typ := c.typeResolver.Substitute(result.Type()) c.Printf("%s = %s;", c.objectName(result), c.translateExpr(c.zeroValue(typ)).String()) id := ast.NewIdent("") @@ -890,7 +924,7 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, if len(c.Blocking) != 0 { deferSuffix += " $s = -1;" } - if c.resultNames == nil && c.sig.Results().Len() > 0 { + if c.resultNames == nil && c.sig.HasResults() { deferSuffix += fmt.Sprintf(" return%s;", c.translateResults(nil)) } deferSuffix += " } finally { $callDeferred($deferred, $err);" diff --git a/compiler/statements.go b/compiler/statements.go index fa203a5b3..95e5ca467 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -773,7 +773,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { } func (fc *funcContext) translateResults(results []ast.Expr) string { - tuple := fc.typeResolver.Substitute(fc.sig.Results()).(*types.Tuple) + tuple := fc.typeResolver.Substitute(fc.sig.Sig.Results()).(*types.Tuple) switch tuple.Len() { case 0: return "" diff --git a/compiler/typesutil/signature.go b/compiler/typesutil/signature.go new file mode 100644 index 000000000..0a79432cb --- /dev/null +++ b/compiler/typesutil/signature.go @@ -0,0 +1,67 @@ +package typesutil + +import ( + "fmt" + "go/types" +) + +// Signature is a helper that provides convenient access to function +// signature type information. +type Signature struct { + Sig *types.Signature +} + +// RequiredParams returns the number of required parameters in the function signature. +func (st Signature) RequiredParams() int { + l := st.Sig.Params().Len() + if st.Sig.Variadic() { + return l - 1 // Last parameter is a slice of variadic params. + } + return l +} + +// VariadicType returns the slice-type corresponding to the signature's variadic +// parameter, or nil of the signature is not variadic. With the exception of +// the special-case `append([]byte{}, "string"...)`, the returned type is +// `*types.Slice` and `.Elem()` method can be used to get the type of individual +// arguments. +func (st Signature) VariadicType() types.Type { + if !st.Sig.Variadic() { + return nil + } + return st.Sig.Params().At(st.Sig.Params().Len() - 1).Type() +} + +// Param returns the expected argument type for the i'th argument position. +// +// This function is able to return correct expected types for variadic calls +// both when ellipsis syntax (e.g. myFunc(requiredArg, optionalArgSlice...)) +// is used and when optional args are passed individually. +// +// The returned types may differ from the actual argument expression types if +// there is an implicit type conversion involved (e.g. passing a struct into a +// function that expects an interface). +func (st Signature) Param(i int, ellipsis bool) types.Type { + if i < st.RequiredParams() { + return st.Sig.Params().At(i).Type() + } + if !st.Sig.Variadic() { + // This should never happen if the code was type-checked successfully. + panic(fmt.Errorf("tried to access parameter %d of a non-variadic signature %s", i, st.Sig)) + } + if ellipsis { + return st.VariadicType() + } + return st.VariadicType().(*types.Slice).Elem() +} + +// HasResults returns true if the function signature returns something. +func (st Signature) HasResults() bool { + return st.Sig.Results().Len() > 0 +} + +// HasNamedResults returns true if the function signature returns something and +// returned results are names (e.g. `func () (val int, err error)`). +func (st Signature) HasNamedResults() bool { + return st.HasResults() && st.Sig.Results().At(0).Name() != "" +} diff --git a/compiler/typesutil/signature_test.go b/compiler/typesutil/signature_test.go new file mode 100644 index 000000000..a6d159687 --- /dev/null +++ b/compiler/typesutil/signature_test.go @@ -0,0 +1,166 @@ +package typesutil + +import ( + "go/token" + "go/types" + "testing" +) + +func TestSignature_RequiredParams(t *testing.T) { + tests := []struct { + descr string + sig *types.Signature + want int + }{{ + descr: "regular signature", + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, false), + want: 3, + }, { + descr: "variadic signature", + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, true /*variadic*/), + want: 2, + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + sig := Signature{Sig: test.sig} + got := sig.RequiredParams() + if got != test.want { + t.Errorf("Got: {%s}.RequiredParams() = %d. Want: %d.", test.sig, got, test.want) + } + }) + } +} + +func TestSignature_VariadicType(t *testing.T) { + tests := []struct { + descr string + sig *types.Signature + want types.Type + }{{ + descr: "regular signature", + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, false), + want: nil, + }, { + descr: "variadic signature", + sig: types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, true /*variadic*/), + want: types.NewSlice(types.Typ[types.String]), + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + sig := Signature{Sig: test.sig} + got := sig.VariadicType() + if !types.Identical(got, test.want) { + t.Errorf("Got: {%s}.VariadicType() = %v. Want: %v.", test.sig, got, test.want) + } + }) + } +} + +func TestSignature_Param(t *testing.T) { + sig := types.NewSignatureType(nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]), + types.NewVar(token.NoPos, nil, "b", types.Typ[types.Byte]), + types.NewVar(token.NoPos, nil, "c", types.NewSlice(types.Typ[types.String])), + ), nil, true /*variadic*/) + + tests := []struct { + descr string + param int + ellipsis bool + want types.Type + }{{ + descr: "required param", + param: 1, + want: types.Typ[types.Byte], + }, { + descr: "variadic param", + param: 2, + want: types.Typ[types.String], + }, { + descr: "variadic param repeated", + param: 3, + want: types.Typ[types.String], + }, { + descr: "variadic param with ellipsis", + param: 2, + ellipsis: true, + want: types.NewSlice(types.Typ[types.String]), + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + sig := Signature{Sig: sig} + got := sig.Param(test.param, test.ellipsis) + if !types.Identical(got, test.want) { + t.Errorf("Got: {%s}.Param(%v, %v) = %v. Want: %v.", sig, test.param, test.ellipsis, got, test.want) + } + }) + } +} + +func TestSignature_HasXResults(t *testing.T) { + tests := []struct { + descr string + sig *types.Signature + hasResults bool + hasNamedResults bool + }{{ + descr: "no results", + sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple(), false), + hasResults: false, + hasNamedResults: false, + }, { + descr: "anonymous result", + sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "", types.Typ[types.String]), + ), false), + hasResults: true, + hasNamedResults: false, + }, { + descr: "named result", + sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "s", types.Typ[types.String]), + ), false), + hasResults: true, + hasNamedResults: true, + }, { + descr: "underscore named result", + sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple( + types.NewVar(token.NoPos, nil, "_", types.Typ[types.String]), + ), false), + hasResults: true, + hasNamedResults: true, + }} + + for _, test := range tests { + t.Run(test.descr, func(t *testing.T) { + sig := Signature{Sig: test.sig} + gotHasResults := sig.HasResults() + if gotHasResults != test.hasResults { + t.Errorf("Got: {%s}.HasResults() = %v. Want: %v.", test.sig, gotHasResults, test.hasResults) + } + gotHasNamedResults := sig.HasNamedResults() + if gotHasNamedResults != test.hasNamedResults { + t.Errorf("Got: {%s}.HasResults() = %v. Want: %v.", test.sig, gotHasNamedResults, test.hasNamedResults) + } + }) + } +} diff --git a/compiler/utils.go b/compiler/utils.go index 95c791245..5a1f947b5 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -136,7 +136,7 @@ func (fc *funcContext) expandTupleArgs(argExprs []ast.Expr) []ast.Expr { func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, ellipsis bool) []string { argExprs = fc.expandTupleArgs(argExprs) - sigTypes := signatureTypes{Sig: sig} + sigTypes := typesutil.Signature{Sig: sig} if sig.Variadic() && len(argExprs) == 0 { return []string{fmt.Sprintf("%s.nil", fc.typeName(sigTypes.VariadicType()))} @@ -818,56 +818,6 @@ func formatJSStructTagVal(jsTag string) string { return "." + jsTag } -// signatureTypes is a helper that provides convenient access to function -// signature type information. -type signatureTypes struct { - Sig *types.Signature -} - -// RequiredParams returns the number of required parameters in the function signature. -func (st signatureTypes) RequiredParams() int { - l := st.Sig.Params().Len() - if st.Sig.Variadic() { - return l - 1 // Last parameter is a slice of variadic params. - } - return l -} - -// VariadicType returns the slice-type corresponding to the signature's variadic -// parameter, or nil of the signature is not variadic. With the exception of -// the special-case `append([]byte{}, "string"...)`, the returned type is -// `*types.Slice` and `.Elem()` method can be used to get the type of individual -// arguments. -func (st signatureTypes) VariadicType() types.Type { - if !st.Sig.Variadic() { - return nil - } - return st.Sig.Params().At(st.Sig.Params().Len() - 1).Type() -} - -// Returns the expected argument type for the i'th argument position. -// -// This function is able to return correct expected types for variadic calls -// both when ellipsis syntax (e.g. myFunc(requiredArg, optionalArgSlice...)) -// is used and when optional args are passed individually. -// -// The returned types may differ from the actual argument expression types if -// there is an implicit type conversion involved (e.g. passing a struct into a -// function that expects an interface). -func (st signatureTypes) Param(i int, ellipsis bool) types.Type { - if i < st.RequiredParams() { - return st.Sig.Params().At(i).Type() - } - if !st.Sig.Variadic() { - // This should never happen if the code was type-checked successfully. - panic(fmt.Errorf("tried to access parameter %d of a non-variadic signature %s", i, st.Sig)) - } - if ellipsis { - return st.VariadicType() - } - return st.VariadicType().(*types.Slice).Elem() -} - // ErrorAt annotates an error with a position in the source code. func ErrorAt(err error, fset *token.FileSet, pos token.Pos) error { return fmt.Errorf("%s: %w", fset.Position(pos), err) From 038d2817c4b148ca623d70ce6cf600b8878ce42f Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 14 Apr 2024 00:06:43 +0100 Subject: [PATCH 3/4] Rename and document functions responsible for identifier generation. These functions provide mappings from Go entities (variables, types, functions, etc.) to JS identifiers assigned to them. This commit adds documentation comments and renames a couple of methods to better match their role. (based on commit 47679137a52bcc73aaf77535f747c8d3404c287a) --- compiler/expressions.go | 36 ++++++++++++++++++------------------ compiler/package.go | 6 +++--- compiler/statements.go | 34 +++++++++++++++++----------------- compiler/utils.go | 40 ++++++++++++++++++++++++++++++++-------- 4 files changed, 70 insertions(+), 46 deletions(-) diff --git a/compiler/expressions.go b/compiler/expressions.go index d113a7774..2768e3d2a 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -398,14 +398,14 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if isUnsigned(basic) { shift = ">>>" } - return fc.formatExpr(`(%1s = %2e / %3e, (%1s === %1s && %1s !== 1/0 && %1s !== -1/0) ? %1s %4s 0 : $throwRuntimeError("integer divide by zero"))`, fc.newVariable("_q"), e.X, e.Y, shift) + return fc.formatExpr(`(%1s = %2e / %3e, (%1s === %1s && %1s !== 1/0 && %1s !== -1/0) ? %1s %4s 0 : $throwRuntimeError("integer divide by zero"))`, fc.newLocalVariable("_q"), e.X, e.Y, shift) } if basic.Kind() == types.Float32 { return fc.fixNumber(fc.formatExpr("%e / %e", e.X, e.Y), basic) } return fc.formatExpr("%e / %e", e.X, e.Y) case token.REM: - return fc.formatExpr(`(%1s = %2e %% %3e, %1s === %1s ? %1s : $throwRuntimeError("integer divide by zero"))`, fc.newVariable("_r"), e.X, e.Y) + return fc.formatExpr(`(%1s = %2e %% %3e, %1s === %1s ? %1s : $throwRuntimeError("integer divide by zero"))`, fc.newLocalVariable("_r"), e.X, e.Y) case token.SHL, token.SHR: op := e.Op.String() if e.Op == token.SHR && isUnsigned(basic) { @@ -421,7 +421,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if e.Op == token.SHR && !isUnsigned(basic) { return fc.fixNumber(fc.formatParenExpr("%e >> $min(%f, 31)", e.X, e.Y), basic) } - y := fc.newVariable("y") + y := fc.newLocalVariable("y") return fc.fixNumber(fc.formatExpr("(%s = %f, %s < 32 ? (%e %s %s) : 0)", y, e.Y, y, e.X, op, y), basic) case token.AND, token.OR: if isUnsigned(basic) { @@ -444,7 +444,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if fc.Blocking[e.Y] { skipCase := fc.caseCounter fc.caseCounter++ - resultVar := fc.newVariable("_v") + resultVar := fc.newLocalVariable("_v") fc.Printf("if (!(%s)) { %s = false; $s = %d; continue s; }", fc.translateExpr(e.X), resultVar, skipCase) fc.Printf("%s = %s; case %d:", resultVar, fc.translateExpr(e.Y), skipCase) return fc.formatExpr("%s", resultVar) @@ -454,7 +454,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if fc.Blocking[e.Y] { skipCase := fc.caseCounter fc.caseCounter++ - resultVar := fc.newVariable("_v") + resultVar := fc.newLocalVariable("_v") fc.Printf("if (%s) { %s = true; $s = %d; continue s; }", fc.translateExpr(e.X), resultVar, skipCase) fc.Printf("%s = %s; case %d:", resultVar, fc.translateExpr(e.Y), skipCase) return fc.formatExpr("%s", resultVar) @@ -513,7 +513,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { if _, isTuple := exprType.(*types.Tuple); isTuple { return fc.formatExpr( `(%1s = $mapIndex(%2e,%3s), %1s !== undefined ? [%1s.v, true] : [%4e, false])`, - fc.newVariable("_entry"), + fc.newLocalVariable("_entry"), e.X, key, fc.zeroValue(t.Elem()), @@ -521,7 +521,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } return fc.formatExpr( `(%1s = $mapIndex(%2e,%3s), %1s !== undefined ? %1s.v : %4e)`, - fc.newVariable("_entry"), + fc.newLocalVariable("_entry"), e.X, key, fc.zeroValue(t.Elem()), @@ -689,13 +689,13 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { case "Call": if id, ok := fc.identifierConstant(e.Args[0]); ok { if e.Ellipsis.IsValid() { - objVar := fc.newVariable("obj") + objVar := fc.newLocalVariable("obj") return fc.formatExpr("(%s = %s, %s.%s.apply(%s, %s))", objVar, recv, objVar, id, objVar, externalizeExpr(e.Args[1])) } return fc.formatExpr("%s(%s)", globalRef(id), externalizeArgs(e.Args[1:])) } if e.Ellipsis.IsValid() { - objVar := fc.newVariable("obj") + objVar := fc.newLocalVariable("obj") return fc.formatExpr("(%s = %s, %s[$externalize(%e, $String)].apply(%s, %s))", objVar, recv, objVar, e.Args[0], objVar, externalizeExpr(e.Args[1])) } return fc.formatExpr("%s[$externalize(%e, $String)](%s)", recv, e.Args[0], externalizeArgs(e.Args[1:])) @@ -842,7 +842,7 @@ func (fc *funcContext) translateCall(e *ast.CallExpr, sig *types.Signature, fun fc.caseCounter++ returnVar := "$r" if sig.Results().Len() != 0 { - returnVar = fc.newVariable("_r") + returnVar = fc.newLocalVariable("_r") } fc.Printf("%[1]s = %[2]s(%[3]s); /* */ $s = %[4]d; case %[4]d: if($c) { $c = false; %[1]s = %[1]s.$blk(); } if (%[1]s && %[1]s.$blk !== undefined) { break s; }", returnVar, fun, strings.Join(args, ", "), resumeCase) if sig.Results().Len() != 0 { @@ -891,7 +891,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression, ellipsis := expr.Ellipsis for i := range expr.Args { - v := fc.newVariable("_arg") + v := fc.newLocalVariable("_arg") vars[i] = v // Subtle: the proxy lambda argument needs to be assigned with the type // that the original function expects, and not with the argument @@ -1172,8 +1172,8 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type } if ptr, isPtr := fc.typeOf(expr).(*types.Pointer); fc.pkgCtx.Pkg.Path() == "syscall" && isPtr { if s, isStruct := ptr.Elem().Underlying().(*types.Struct); isStruct { - array := fc.newVariable("_array") - target := fc.newVariable("_struct") + array := fc.newLocalVariable("_array") + target := fc.newLocalVariable("_struct") fc.Printf("%s = new Uint8Array(%d);", array, sizes32.Sizeof(s)) fc.Delayed(func() { fc.Printf("%s = %s, %s;", target, fc.translateExpr(expr), fc.loadStruct(array, target, s)) @@ -1221,8 +1221,8 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type // struct pointer when handling syscalls. // TODO(nevkontakte): Add a runtime assertion that the unsafe.Pointer is // indeed pointing at a byte array. - array := fc.newVariable("_array") - target := fc.newVariable("_struct") + array := fc.newLocalVariable("_array") + target := fc.newLocalVariable("_struct") return fc.formatExpr("(%s = %e, %s = %e, %s, %s)", array, expr, target, fc.zeroValue(t.Elem()), fc.loadStruct(array, target, ptrElType), target) } // Convert between structs of different types but identical layouts, @@ -1244,7 +1244,7 @@ func (fc *funcContext) translateConversion(expr ast.Expr, desiredType types.Type // type iPtr *int; var c int = 42; println((iPtr)(&c)); // TODO(nevkontakte): Are there any other cases that fall into this case? exprTypeElem := exprType.Underlying().(*types.Pointer).Elem() - ptrVar := fc.newVariable("_ptr") + ptrVar := fc.newLocalVariable("_ptr") getterConv := fc.translateConversion(fc.setType(&ast.StarExpr{X: fc.newIdent(ptrVar, exprType)}, exprTypeElem), t.Elem()) setterConv := fc.translateConversion(fc.newIdent("$v", t.Elem()), exprTypeElem) return fc.formatExpr("(%1s = %2e, new %3s(function() { return %4s; }, function($v) { %1s.$set(%5s); }, %1s.$target))", ptrVar, expr, fc.typeName(desiredType), getterConv, setterConv) @@ -1311,7 +1311,7 @@ func (fc *funcContext) translateConversionToSlice(expr ast.Expr, desiredType typ } func (fc *funcContext) loadStruct(array, target string, s *types.Struct) string { - view := fc.newVariable("_view") + view := fc.newLocalVariable("_view") code := fmt.Sprintf("%s = new DataView(%s.buffer, %s.byteOffset)", view, array, array) var fields []*types.Var var collectFields func(s *types.Struct, path string) @@ -1441,7 +1441,7 @@ func (fc *funcContext) formatExprInternal(format string, a []interface{}, parens out.WriteByte('(') parens = false } - v := fc.newVariable("x") + v := fc.newLocalVariable("x") out.WriteString(v + " = " + fc.translateExpr(e.(ast.Expr)).String() + ", ") vars[i] = v } diff --git a/compiler/package.go b/compiler/package.go index 79230b4a8..07eb43cf8 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -297,7 +297,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor // but now we do it here to maintain previous behavior. continue } - funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariableWithLevel(importedPkg.Name(), true) + funcCtx.pkgCtx.pkgVars[importedPkg.Path()] = funcCtx.newVariable(importedPkg.Name(), true) importedPaths = append(importedPaths, importedPkg.Path()) } sort.Strings(importedPaths) @@ -832,12 +832,12 @@ func translateFunction(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, var params []string for _, param := range typ.Params.List { if len(param.Names) == 0 { - params = append(params, c.newVariable("param")) + params = append(params, c.newLocalVariable("param")) continue } for _, ident := range param.Names { if isBlank(ident) { - params = append(params, c.newVariable("param")) + params = append(params, c.newLocalVariable("param")) continue } params = append(params, c.objectName(c.pkgCtx.Defs[ident])) diff --git a/compiler/statements.go b/compiler/statements.go index 95e5ca467..3f228963e 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -125,7 +125,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if s.Init != nil { fc.translateStmt(s.Init, nil) } - refVar := fc.newVariable("_ref") + refVar := fc.newLocalVariable("_ref") var expr ast.Expr switch a := s.Assign.(type) { case *ast.AssignStmt: @@ -188,14 +188,14 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *ast.RangeStmt: - refVar := fc.newVariable("_ref") + refVar := fc.newLocalVariable("_ref") fc.Printf("%s = %s;", refVar, fc.translateExpr(s.X)) switch t := fc.typeOf(s.X).Underlying().(type) { case *types.Basic: - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) - runeVar := fc.newVariable("_rune") + runeVar := fc.newLocalVariable("_rune") fc.translateLoopingStmt(func() string { return iVar + " < " + refVar + ".length" }, s.Body, func() { fc.Printf("%s = $decodeRune(%s, %s);", runeVar, refVar, iVar) if !isBlank(s.Key) { @@ -209,16 +209,16 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *types.Map: - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) - keysVar := fc.newVariable("_keys") + keysVar := fc.newLocalVariable("_keys") fc.Printf("%s = %s ? %s.keys() : undefined;", keysVar, refVar, refVar) - sizeVar := fc.newVariable("_size") + sizeVar := fc.newLocalVariable("_size") fc.Printf("%s = %s ? %s.size : 0;", sizeVar, refVar, refVar) fc.translateLoopingStmt(func() string { return iVar + " < " + sizeVar }, s.Body, func() { - keyVar := fc.newVariable("_key") - entryVar := fc.newVariable("_entry") + keyVar := fc.newLocalVariable("_key") + entryVar := fc.newLocalVariable("_entry") fc.Printf("%s = %s.next().value;", keyVar, keysVar) fc.Printf("%s = %s.get(%s);", entryVar, refVar, keyVar) fc.translateStmt(&ast.IfStmt{ @@ -249,7 +249,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { length = refVar + ".$length" elemType = t2.Elem() } - iVar := fc.newVariable("_i") + iVar := fc.newLocalVariable("_i") fc.Printf("%s = 0;", iVar) fc.translateLoopingStmt(func() string { return iVar + " < " + length }, s.Body, func() { if !isBlank(s.Key) { @@ -266,7 +266,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { }, label, fc.Flattened[s]) case *types.Chan: - okVar := fc.newIdent(fc.newVariable("_ok"), types.Typ[types.Bool]) + okVar := fc.newIdent(fc.newLocalVariable("_ok"), types.Typ[types.Bool]) key := s.Key tok := s.Tok if key == nil { @@ -355,7 +355,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { if rVal != "" { // If returned expression is non empty, evaluate and store it in a // variable to avoid double-execution in case a deferred function blocks. - rVar := fc.newVariable("$r") + rVar := fc.newLocalVariable("$r") fc.Printf("%s =%s;", rVar, rVal) rVal = " " + rVar } @@ -387,7 +387,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.Printf("%s", fc.translateAssign(lhs, s.Rhs[0], s.Tok == token.DEFINE)) case len(s.Lhs) > 1 && len(s.Rhs) == 1: - tupleVar := fc.newVariable("_tuple") + tupleVar := fc.newLocalVariable("_tuple") fc.Printf("%s = %s;", tupleVar, fc.translateExpr(s.Rhs[0])) tuple := fc.typeOf(s.Rhs[0]).(*types.Tuple) for i, lhs := range s.Lhs { @@ -399,7 +399,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { case len(s.Lhs) == len(s.Rhs): tmpVars := make([]string, len(s.Rhs)) for i, rhs := range s.Rhs { - tmpVars[i] = fc.newVariable("_tmp") + tmpVars[i] = fc.newLocalVariable("_tmp") if isBlank(astutil.RemoveParens(s.Lhs[i])) { fc.Printf("$unused(%s);", fc.translateExpr(rhs)) continue @@ -478,7 +478,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { fc.translateStmt(&ast.ExprStmt{X: call}, label) case *ast.SelectStmt: - selectionVar := fc.newVariable("_selection") + selectionVar := fc.newLocalVariable("_selection") var channels []string var caseClauses []*ast.CaseClause flattened := false @@ -704,7 +704,7 @@ func (fc *funcContext) translateAssign(lhs, rhs ast.Expr, define bool) string { if typesutil.IsJsObject(fc.typeOf(l.Index)) { fc.pkgCtx.errList = append(fc.pkgCtx.errList, types.Error{Fset: fc.pkgCtx.fileSet, Pos: l.Index.Pos(), Msg: "cannot use js.Object as map key"}) } - keyVar := fc.newVariable("_key") + keyVar := fc.newLocalVariable("_key") return fmt.Sprintf( `%s = %s; (%s || $throwRuntimeError("assignment to entry in nil map")).set(%s.keyFor(%s), { k: %s, v: %s });`, keyVar, @@ -799,7 +799,7 @@ func (fc *funcContext) translateResults(results []ast.Expr) string { return " " + resultExpr } - tmpVar := fc.newVariable("_returncast") + tmpVar := fc.newLocalVariable("_returncast") fc.Printf("%s = %s;", tmpVar, resultExpr) // Not all the return types matched, map everything out for implicit casting diff --git a/compiler/utils.go b/compiler/utils.go index 5a1f947b5..c1c5941f5 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -124,7 +124,7 @@ func (fc *funcContext) expandTupleArgs(argExprs []ast.Expr) []ast.Expr { return argExprs } - tupleVar := fc.newVariable("_tuple") + tupleVar := fc.newLocalVariable("_tuple") fc.Printf("%s = %s;", tupleVar, fc.translateExpr(argExprs[0])) argExprs = make([]ast.Expr, tuple.Len()) for i := range argExprs { @@ -152,7 +152,7 @@ func (fc *funcContext) translateArgs(sig *types.Signature, argExprs []ast.Expr, arg := fc.translateImplicitConversionWithCloning(argExpr, sigTypes.Param(i, ellipsis)).String() if preserveOrder && fc.pkgCtx.Types[argExpr].Value == nil { - argVar := fc.newVariable("_arg") + argVar := fc.newLocalVariable("_arg") fc.Printf("%s = %s;", argVar, arg) arg = argVar } @@ -247,11 +247,26 @@ func (fc *funcContext) newConst(t types.Type, value constant.Value) ast.Expr { return id } -func (fc *funcContext) newVariable(name string) string { - return fc.newVariableWithLevel(name, false) +// newLocalVariable assigns a new JavaScript variable name for the given Go +// local variable name. In this context "local" means "in scope of the current" +// functionContext. +func (fc *funcContext) newLocalVariable(name string) string { + return fc.newVariable(name, false) } -func (fc *funcContext) newVariableWithLevel(name string, pkgLevel bool) string { +// newVariable assigns a new JavaScript variable name for the given Go variable +// or type. +// +// If there is already a variable with the same name visible in the current +// function context (e.g. due to shadowing), the returned name will be suffixed +// with a number to prevent conflict. This is necessary because Go name +// resolution scopes differ from var declarations in JS. +// +// If pkgLevel is true, the variable is declared at the package level and added +// to this functionContext, as well as all parents, but not to the list of local +// variables. If false, it is added to this context only, as well as the list of +// local vars. +func (fc *funcContext) newVariable(name string, pkgLevel bool) string { if name == "" { panic("newVariable: empty name") } @@ -373,7 +388,7 @@ func (fc *funcContext) objectName(o types.Object) string { name, ok := fc.assignedObjectName(o) if !ok { pkgLevel := isPkgLevel(o) - name = fc.newVariableWithLevel(o.Name(), pkgLevel) + name = fc.newVariable(o.Name(), pkgLevel) if pkgLevel { fc.root().objectNames[o] = name } else { @@ -405,12 +420,17 @@ func (fc *funcContext) varPtrName(o *types.Var) string { name, ok := fc.pkgCtx.varPtrNames[o] if !ok { - name = fc.newVariableWithLevel(o.Name()+"$ptr", isPkgLevel(o)) + name = fc.newVariable(o.Name()+"$ptr", isPkgLevel(o)) fc.pkgCtx.varPtrNames[o] = name } return name } +// typeName returns a JS identifier name for the given Go type. +// +// For the built-in types it returns identifiers declared in the prelude. For +// all user-defined or composite types it creates a unique JS identifier and +// will return it on all subsequent calls for the type. func (fc *funcContext) typeName(ty types.Type) string { switch t := ty.(type) { case *types.Basic: @@ -430,10 +450,14 @@ func (fc *funcContext) typeName(ty types.Type) string { } } + // For anonymous composite types, generate a synthetic package-level type + // declaration, which will be reused for all instances of this time. This + // improves performance, since runtime won't have to synthesize the same type + // repeatedly. anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName) if !ok { fc.initArgs(ty) // cause all embedded types to be registered - varName := fc.newVariableWithLevel(strings.ToLower(typeKind(ty)[5:])+"Type", true) + varName := fc.newVariable(strings.ToLower(typeKind(ty)[5:])+"Type", true) anonType = types.NewTypeName(token.NoPos, fc.pkgCtx.Pkg, varName, ty) // fake types.TypeName fc.pkgCtx.anonTypes = append(fc.pkgCtx.anonTypes, anonType) fc.pkgCtx.anonTypeMap.Set(ty, anonType) From b0663d925233606191a8dd353a43d745c50404aa Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sun, 1 Jan 2023 18:08:56 +0000 Subject: [PATCH 4/4] compiler: factor out utility types for processing Go sources and errors. This is the first step in reducing complexity of the compiler.Compile function. The new `sources` type represents all inputs into the package compilation and simplifies extracting useful information out of them. It is designed to have little business logic of its own and serves as a convenient bridge to other packages like go/types or astrewrite. The ErrorList type is extended with utility methods that reduce verbosity of the calling code. (cherry picked from commit cb5cf572b48c7c0f4a3a2e1964543238fb890356) --- compiler/compiler.go | 16 ----- compiler/errors.go | 68 +++++++++++++++++++++ compiler/linkname.go | 2 +- compiler/package.go | 142 ++++++++++++------------------------------- compiler/sources.go | 123 +++++++++++++++++++++++++++++++++++++ 5 files changed, 230 insertions(+), 121 deletions(-) create mode 100644 compiler/errors.go create mode 100644 compiler/sources.go diff --git a/compiler/compiler.go b/compiler/compiler.go index 1614b8a7f..91578e884 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -33,22 +33,6 @@ func init() { } } -type ErrorList []error - -func (err ErrorList) Error() string { - if len(err) == 0 { - return "" - } - return fmt.Sprintf("%s (and %d more errors)", err[0].Error(), len(err[1:])) -} - -func (err ErrorList) Normalize() error { - if len(err) == 0 { - return nil - } - return err -} - // Archive contains intermediate build outputs of a single package. // // This is a logical equivalent of an object file in traditional compilers. diff --git a/compiler/errors.go b/compiler/errors.go new file mode 100644 index 000000000..48aed48ec --- /dev/null +++ b/compiler/errors.go @@ -0,0 +1,68 @@ +package compiler + +import ( + "errors" + "fmt" +) + +// ErrTooManyErrors is added to the ErrorList by the Trim method. +var ErrTooManyErrors = errors.New("too many errors") + +// ErrorList wraps multiple errors as a single error. +type ErrorList []error + +func (errs ErrorList) Error() string { + if len(errs) == 0 { + return "" + } + return fmt.Sprintf("%s (and %d more errors)", errs[0].Error(), len(errs[1:])) +} + +// ErrOrNil returns nil if ErrorList is empty, or the error otherwise. +func (errs ErrorList) ErrOrNil() error { + if len(errs) == 0 { + return nil + } + return errs +} + +// Append an error to the list. +// +// If err is an instance of ErrorList, the lists are concatenated together, +// otherwise err is appended at the end of the list. If err is nil, the list is +// returned unmodified. +// +// err := DoStuff() +// errList := errList.Append(err) +func (errs ErrorList) Append(err error) ErrorList { + if err == nil { + return errs + } + if err, ok := err.(ErrorList); ok { + return append(errs, err...) + } + return append(errs, err) +} + +// AppendDistinct is similar to Append, but doesn't append the error if it has +// the same message as the last error on the list. +func (errs ErrorList) AppendDistinct(err error) ErrorList { + if l := len(errs); l > 0 { + if prev := errs[l-1]; prev != nil && err.Error() == prev.Error() { + return errs // The new error is the same as the last one, skip it. + } + } + + return errs.Append(err) +} + +// Trim the error list if it has more than limit errors. If the list is trimmed, +// all extraneous errors are replaced with a single ErrTooManyErrors, making the +// returned ErrorList length of limit+1. +func (errs ErrorList) Trim(limit int) ErrorList { + if len(errs) <= limit { + return errs + } + + return append(errs[:limit], ErrTooManyErrors) +} diff --git a/compiler/linkname.go b/compiler/linkname.go index 0bb2b3509..c4f15a23e 100644 --- a/compiler/linkname.go +++ b/compiler/linkname.go @@ -112,7 +112,7 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go } } - return directives, errs.Normalize() + return directives, errs.ErrOrNil() } // goLinknameSet is a utility that enables quick lookup of whether a decl is diff --git a/compiler/package.go b/compiler/package.go index 07eb43cf8..f82cd798a 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -18,7 +18,6 @@ import ( "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/gopherjs/gopherjs/internal/experiments" - "github.com/neelance/astrewrite" "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/types/typeutil" ) @@ -108,34 +107,20 @@ type flowData struct { endCase int } +// ImportContext provides access to information about imported packages. type ImportContext struct { + // Mapping for an absolute import path to the package type information. Packages map[string]*types.Package - Import func(string) (*Archive, error) -} - -// packageImporter implements go/types.Importer interface. -type packageImporter struct { - importContext *ImportContext - importError *error // A pointer to importError in Compile. -} - -func (pi packageImporter) Import(path string) (*types.Package, error) { - if path == "unsafe" { - return types.Unsafe, nil - } - - a, err := pi.importContext.Import(path) - if err != nil { - if *pi.importError == nil { - // If import failed, show first error of import only (https://github.com/gopherjs/gopherjs/issues/119). - *pi.importError = err - } - return nil, err - } - - return pi.importContext.Packages[a.ImportPath], nil + // Import returns a previously compiled Archive for a dependency package. If + // the Import() call was successful, the corresponding entry must be added to + // the Packages map. + Import func(importPath string) (*Archive, error) } +// Compile the provided Go sources as a single package. +// +// Import path must be the absolute import path for a package. Provided sources +// are always sorted by name to ensure reproducible JavaScript output. func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, importContext *ImportContext, minify bool) (_ *Archive, err error) { defer func() { e := recover() @@ -152,89 +137,29 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor err = bailout(fmt.Errorf("unexpected compiler panic while building package %q: %v", importPath, e)) }() - // Files must be in the same order to get reproducible JS - sort.Slice(files, func(i, j int) bool { - return fileSet.File(files[i].Pos()).Name() > fileSet.File(files[j].Pos()).Name() - }) - - typesInfo := &types.Info{ - Types: make(map[ast.Expr]types.TypeAndValue), - Defs: make(map[*ast.Ident]types.Object), - Uses: make(map[*ast.Ident]types.Object), - Implicits: make(map[ast.Node]types.Object), - Selections: make(map[*ast.SelectorExpr]*types.Selection), - Scopes: make(map[ast.Node]*types.Scope), - Instances: make(map[*ast.Ident]types.Instance), - } - - var errList ErrorList - - // Extract all go:linkname compiler directives from the package source. - goLinknames := []GoLinkname{} - for _, file := range files { - found, err := parseGoLinknames(fileSet, importPath, file) - if err != nil { - if errs, ok := err.(ErrorList); ok { - errList = append(errList, errs...) - } else { - errList = append(errList, err) - } - } - goLinknames = append(goLinknames, found...) - } + srcs := sources{ + ImportPath: importPath, + Files: files, + FileSet: fileSet, + }.Sort() - var importError error - var previousErr error - config := &types.Config{ - Context: types.NewContext(), - Importer: packageImporter{ - importContext: importContext, - importError: &importError, - }, - Sizes: sizes32, - Error: func(err error) { - if previousErr != nil && previousErr.Error() == err.Error() { - return - } - errList = append(errList, err) - previousErr = err - }, - } - typesPkg, err := config.Check(importPath, fileSet, files, typesInfo) - if importError != nil { - return nil, importError - } - if errList != nil { - if len(errList) > 10 { - pos := token.NoPos - if last, ok := errList[9].(types.Error); ok { - pos = last.Pos - } - errList = append(errList[:10], types.Error{Fset: fileSet, Pos: pos, Msg: "too many errors"}) - } - return nil, errList - } + tContext := types.NewContext() + typesInfo, typesPkg, err := srcs.TypeCheck(importContext, tContext) if err != nil { return nil, err } if genErr := typeparams.RequiresGenericsSupport(typesInfo); genErr != nil && !experiments.Env.Generics { return nil, fmt.Errorf("package %s requires generics support (https://github.com/gopherjs/gopherjs/issues/1013): %w", importPath, genErr) } - importContext.Packages[importPath] = typesPkg + importContext.Packages[srcs.ImportPath] = typesPkg - exportData := new(bytes.Buffer) - if err := gcexportdata.Write(exportData, nil, typesPkg); err != nil { - return nil, fmt.Errorf("failed to write export data: %v", err) - } - encodedFileSet := new(bytes.Buffer) - if err := fileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { + // Extract all go:linkname compiler directives from the package source. + goLinknames, err := srcs.ParseGoLinknames() + if err != nil { return nil, err } - simplifiedFiles := make([]*ast.File, len(files)) - for i, file := range files { - simplifiedFiles[i] = astrewrite.Simplify(file, typesInfo, false) - } + srcs = srcs.Simplified(typesInfo) isBlocking := func(f *types.Func) bool { archive, err := importContext.Import(f.Pkg().Path()) @@ -251,31 +176,31 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor } tc := typeparams.Collector{ - TContext: config.Context, + TContext: tContext, Info: typesInfo, Instances: &typeparams.PackageInstanceSets{}, } - tc.Scan(typesPkg, simplifiedFiles...) + tc.Scan(typesPkg, srcs.Files...) instancesByObj := map[types.Object][]typeparams.Instance{} for _, inst := range tc.Instances.Pkg(typesPkg).Values() { instancesByObj[inst.Object] = append(instancesByObj[inst.Object], inst) } - pkgInfo := analysis.AnalyzePkg(simplifiedFiles, fileSet, typesInfo, typesPkg, isBlocking) + pkgInfo := analysis.AnalyzePkg(srcs.Files, fileSet, typesInfo, typesPkg, isBlocking) funcCtx := &funcContext{ FuncInfo: pkgInfo.InitFuncInfo, pkgCtx: &pkgContext{ Info: pkgInfo, additionalSelections: make(map[*ast.SelectorExpr]typesutil.Selection), - typesCtx: config.Context, + typesCtx: tContext, pkgVars: make(map[string]string), varPtrNames: make(map[*types.Var]string), escapingVars: make(map[*types.Var]bool), indentation: 1, dependencies: make(map[types.Object]bool), minify: minify, - fileSet: fileSet, + fileSet: srcs.FileSet, instanceSet: tc.Instances, }, allVars: make(map[string]int), @@ -315,7 +240,7 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor var functions []*ast.FuncDecl var vars []*types.Var - for _, file := range simplifiedFiles { + for _, file := range srcs.Files { for _, decl := range file.Decls { switch d := decl.(type) { case *ast.FuncDecl: @@ -669,8 +594,17 @@ func Compile(importPath string, files []*ast.File, fileSet *token.FileSet, impor return nil, funcCtx.pkgCtx.errList } + exportData := new(bytes.Buffer) + if err := gcexportdata.Write(exportData, nil, typesPkg); err != nil { + return nil, fmt.Errorf("failed to write export data: %w", err) + } + encodedFileSet := new(bytes.Buffer) + if err := srcs.FileSet.Write(json.NewEncoder(encodedFileSet).Encode); err != nil { + return nil, err + } + return &Archive{ - ImportPath: importPath, + ImportPath: srcs.ImportPath, Name: typesPkg.Name(), Imports: importedPaths, ExportData: exportData.Bytes(), diff --git a/compiler/sources.go b/compiler/sources.go new file mode 100644 index 000000000..cfc96779b --- /dev/null +++ b/compiler/sources.go @@ -0,0 +1,123 @@ +package compiler + +import ( + "go/ast" + "go/token" + "go/types" + "sort" + + "github.com/neelance/astrewrite" +) + +// sources is a slice of parsed Go sources. +// +// Note that the sources would normally belong to a single logical Go package, +// but they don't have to be a real Go package (i.e. found on the file system) +// or represent a complete package (i.e. it could be only a few source files +// compiled by `gopherjs build foo.go bar.go`). +type sources struct { + // ImportPath representing the sources, if exists. May be empty for "virtual" + // packages like testmain or playground-generated package. + ImportPath string + Files []*ast.File + FileSet *token.FileSet +} + +// Sort the Files slice by the original source name to ensure consistent order +// of processing. This is required for reproducible JavaScript output. +// +// Note this function mutates the original slice. +func (s sources) Sort() sources { + sort.Slice(s.Files, func(i, j int) bool { + return s.FileSet.File(s.Files[i].Pos()).Name() > s.FileSet.File(s.Files[j].Pos()).Name() + }) + return s +} + +// Simplify returns a new sources instance with each Files entry processed by +// astrewrite.Simplify. +func (s sources) Simplified(typesInfo *types.Info) sources { + simplified := sources{ + ImportPath: s.ImportPath, + FileSet: s.FileSet, + } + for _, file := range s.Files { + simplified.Files = append(simplified.Files, astrewrite.Simplify(file, typesInfo, false)) + } + return simplified +} + +// TypeCheck the sources. Returns information about declared package types and +// type information for the supplied AST. +func (s sources) TypeCheck(importContext *ImportContext, tContext *types.Context) (*types.Info, *types.Package, error) { + const errLimit = 10 // Max number of type checking errors to return. + + typesInfo := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), + } + + var typeErrs ErrorList + + importer := packageImporter{ImportContext: importContext} + + config := &types.Config{ + Context: tContext, + Importer: &importer, + Sizes: sizes32, + Error: func(err error) { typeErrs = typeErrs.AppendDistinct(err) }, + } + typesPkg, err := config.Check(s.ImportPath, s.FileSet, s.Files, typesInfo) + // If we encountered any import errors, it is likely that the other type errors + // are not meaningful and would be resolved by fixing imports. Return them + // separately, if any. https://github.com/gopherjs/gopherjs/issues/119. + if importer.Errors.ErrOrNil() != nil { + return nil, nil, importer.Errors.Trim(errLimit).ErrOrNil() + } + // Return any other type errors. + if typeErrs.ErrOrNil() != nil { + return nil, nil, typeErrs.Trim(errLimit).ErrOrNil() + } + // Any general errors that may have occurred during type checking. + if err != nil { + return nil, nil, err + } + return typesInfo, typesPkg, nil +} + +// ParseGoLinknames extracts all //go:linkname compiler directive from the sources. +func (s sources) ParseGoLinknames() ([]GoLinkname, error) { + goLinknames := []GoLinkname{} + var errs ErrorList + for _, file := range s.Files { + found, err := parseGoLinknames(s.FileSet, s.ImportPath, file) + errs = errs.Append(err) + goLinknames = append(goLinknames, found...) + } + return goLinknames, errs.ErrOrNil() +} + +// packageImporter implements go/types.Importer interface. +type packageImporter struct { + ImportContext *ImportContext + Errors ErrorList +} + +func (pi *packageImporter) Import(path string) (*types.Package, error) { + if path == "unsafe" { + return types.Unsafe, nil + } + + a, err := pi.ImportContext.Import(path) + if err != nil { + pi.Errors = pi.Errors.AppendDistinct(err) + return nil, err + } + + return pi.ImportContext.Packages[a.ImportPath], nil +}