Skip to content

Ability to call overridden functions from the std library #1216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,20 @@ func ImportDir(dir string, mode build.ImportMode, installSuffix string, buildTag
// The native packages are augmented by the contents of natives.FS in the following way.
// The file names do not matter except the usual `_test` suffix. The files for
// native overrides get added to the package (even if they have the same name
// as an existing file from the standard library). For all identifiers that exist
// in the original AND the overrides, the original identifier in the AST gets
// replaced by `_`. New identifiers that don't exist in original package get added.
// as an existing file from the standard library). For function identifiers that exist
// in the original AND the overrides AND that include the following directive in their comment:
// //gopherjs:keep-original, the original identifier in the AST gets prefixed by
// `_gopherjs_original_`. For other identifiers that exist in the original AND the overrides,
// the original identifier gets replaced by `_`. New identifiers that don't exist in original
// package get added.
func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *token.FileSet) ([]*ast.File, []JSFile, error) {
var files []*ast.File
replacedDeclNames := make(map[string]bool)
pruneOriginalFuncs := make(map[string]bool)

type overrideInfo struct {
keepOriginal bool
pruneOriginal bool
}
replacedDeclNames := make(map[string]overrideInfo)

isXTest := strings.HasSuffix(pkg.ImportPath, "_test")
importPath := pkg.ImportPath
Expand Down Expand Up @@ -170,18 +177,20 @@ func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *toke
switch d := decl.(type) {
case *ast.FuncDecl:
k := astutil.FuncKey(d)
replacedDeclNames[k] = true
pruneOriginalFuncs[k] = astutil.PruneOriginal(d)
replacedDeclNames[k] = overrideInfo{
keepOriginal: astutil.KeepOriginal(d),
pruneOriginal: astutil.PruneOriginal(d),
}
case *ast.GenDecl:
switch d.Tok {
case token.TYPE:
for _, spec := range d.Specs {
replacedDeclNames[spec.(*ast.TypeSpec).Name.Name] = true
replacedDeclNames[spec.(*ast.TypeSpec).Name.Name] = overrideInfo{}
}
case token.VAR, token.CONST:
for _, spec := range d.Specs {
for _, name := range spec.(*ast.ValueSpec).Names {
replacedDeclNames[name.Name] = true
replacedDeclNames[name.Name] = overrideInfo{}
}
}
}
Expand Down Expand Up @@ -234,20 +243,26 @@ func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *toke
switch d := decl.(type) {
case *ast.FuncDecl:
k := astutil.FuncKey(d)
if replacedDeclNames[k] {
d.Name = ast.NewIdent("_")
if pruneOriginalFuncs[k] {
if info, ok := replacedDeclNames[k]; ok {
if info.pruneOriginal {
// Prune function bodies, since it may contain code invalid for
// GopherJS and pin unwanted imports.
d.Body = nil
}
if info.keepOriginal {
// Allow overridden function calls
// The standard library implementation of foo() becomes _gopherjs_original_foo()
d.Name.Name = "_gopherjs_original_" + d.Name.Name
} else {
d.Name = ast.NewIdent("_")
}
}
case *ast.GenDecl:
switch d.Tok {
case token.TYPE:
for _, spec := range d.Specs {
s := spec.(*ast.TypeSpec)
if replacedDeclNames[s.Name.Name] {
if _, ok := replacedDeclNames[s.Name.Name]; ok {
s.Name = ast.NewIdent("_")
s.Type = &ast.StructType{Struct: s.Pos(), Fields: &ast.FieldList{}}
s.TypeParams = nil
Expand All @@ -257,7 +272,7 @@ func parseAndAugment(xctx XContext, pkg *PackageData, isTest bool, fileSet *toke
for _, spec := range d.Specs {
s := spec.(*ast.ValueSpec)
for i, name := range s.Names {
if replacedDeclNames[name.Name] {
if _, ok := replacedDeclNames[name.Name]; ok {
s.Names[i] = ast.NewIdent("_")
}
}
Expand Down
20 changes: 20 additions & 0 deletions compiler/astutil/astutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ func PruneOriginal(d *ast.FuncDecl) bool {
return false
}

// KeepOriginal returns true if gopherjs:keep-original directive is present
// before a function decl.
//
// `//gopherjs:keep-original` is a GopherJS-specific directive, which can be
// applied to functions in native overlays and will instruct the augmentation
// logic to expose the original function such that it can be called. For a
// function in the original called `foo`, it will be accessible by the name
// `_gopherjs_original_foo`.
func KeepOriginal(d *ast.FuncDecl) bool {
if d.Doc == nil {
return false
}
for _, c := range d.Doc.List {
if strings.HasPrefix(c.Text, "//gopherjs:keep-original") {
return true
}
}
return false
}

// FindLoopStmt tries to find the loop statement among the AST nodes in the
// |stack| that corresponds to the break/continue statement represented by
// branch.
Expand Down
10 changes: 9 additions & 1 deletion compiler/natives/src/regexp/regexp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import (
"testing"
)

//gopherjs:keep-original
func TestOnePassCutoff(t *testing.T) {
t.Skip() // "Maximum call stack size exceeded" on V8
defer func() {
if r := recover(); r != nil {
t.Log(r)
t.Skip("'Maximum call stack size exceeded' may happen on V8, skipping")
}
}()

_gopherjs_original_TestOnePassCutoff(t)
}