Skip to content
Open
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
71 changes: 70 additions & 1 deletion compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package goja

import (
"fmt"
"github.com/dop251/goja/token"
"sort"

"github.com/dop251/goja/ast"
"github.com/dop251/goja/file"
"github.com/dop251/goja/token"
"github.com/dop251/goja/unistring"
"github.com/go-sourcemap/sourcemap"
)

type blockType int
Expand Down Expand Up @@ -89,6 +90,8 @@ type compiler struct {
codeScratchpad []instruction

stringCache map[unistring.String]Value

debugMode bool // when true, emit debug variable maps for the debugger
}

type binding struct {
Expand Down Expand Up @@ -327,6 +330,7 @@ func (c *compiler) leaveScopeBlock(enter *enterBlock) {
leave := &leaveBlock{
stackSize: enter.stackSize,
popStash: enter.stashSize > 0,
dbgPop: len(enter.dbgNames) > 0,
}
c.emit(leave)
for _, pc := range c.block.breaks {
Expand Down Expand Up @@ -467,6 +471,17 @@ func (p *Program) sourceOffset(pc int) int {
return 0
}

// SetSourceMap attaches a source map to the program. Once set, all position
// resolution (including debugger breakpoint matching and stack traces)
// automatically maps through the source map to original source positions.
// This is useful when the source was transpiled (e.g., TypeScript to JavaScript)
// and you want debugging to work against the original source.
func (p *Program) SetSourceMap(m *sourcemap.Consumer) {
if p.src != nil {
p.src.SetSourceMap(m)
}
}

func (p *Program) addSrcMap(srcPos int) {
if len(p.srcMap) > 0 && p.srcMap[len(p.srcMap)-1].srcPos == srcPos {
return
Expand Down Expand Up @@ -882,6 +897,60 @@ func (s *scope) makeNamesMap() map[unistring.String]uint32 {
return names
}

// makeDebugStashNamesMap builds a stash names map for debugger introspection
// in non-dynamic scopes. Unlike makeNamesMap (which uses binding index and is
// only correct when ALL bindings are in the stash), this uses actual stash
// indices matching the allocation in finaliseVarAlloc.
func (s *scope) makeDebugStashNamesMap() map[unistring.String]uint32 {
var names map[unistring.String]uint32
stashIdx := uint32(0)
for _, b := range s.bindings {
if b.inStash {
if names == nil {
names = make(map[unistring.String]uint32)
}
idx := stashIdx
if b.isConst {
idx |= maskConst
if b.isStrict {
idx |= maskStrict
}
}
if b.isVar {
idx |= maskVar
}
names[b.name] = idx
stashIdx++
}
}
return names
}

// makeDebugRegisterNamesMap builds a debug names map for stack-register (non-stash)
// variables so the debugger can see them. Arguments are encoded with negative
// indices (-(argIndex+1)), locals with sequential non-negative indices.
// When skipInStash is true, bindings that live in the stash are skipped (used
// for functions that have both stash and register variables).
func (s *scope) makeDebugRegisterNamesMap(skipInStash bool) map[unistring.String]int {
names := make(map[unistring.String]int, len(s.bindings))
localIdx := 0
for i, b := range s.bindings {
if b.name == thisBindingName || (skipInStash && b.inStash) {
continue
}
if i < int(s.numArgs) {
names[b.name] = -(i + 1)
} else {
names[b.name] = localIdx
localIdx++
}
}
if len(names) == 0 {
return nil
}
return names
}

func (s *scope) isDynamic() bool {
return s.dynLookup || s.dynamic
}
Expand Down
14 changes: 13 additions & 1 deletion compiler_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,11 @@ func (e *compiledFunctionLiteral) compile() (prg *Program, name unistring.String
}
if s.isDynamic() {
enter1.names = s.makeNamesMap()
} else if e.c.debugMode && stashSize > 0 {
enter1.names = s.makeDebugStashNamesMap()
}
if e.c.debugMode && stackSize > 0 {
enter1.dbgNames = s.makeDebugRegisterNamesMap(true)
}
enter = &enter1
if enterFunc2Mark != -1 {
Expand All @@ -1746,6 +1751,8 @@ func (e *compiledFunctionLiteral) compile() (prg *Program, name unistring.String
}
if s.isDynamic() {
enter1.names = s.makeNamesMap()
} else if e.c.debugMode && stashSize > 0 {
enter1.names = s.makeDebugStashNamesMap()
}
enter = &enter1
if enterFunc2Mark != -1 {
Expand All @@ -1762,10 +1769,15 @@ func (e *compiledFunctionLiteral) compile() (prg *Program, name unistring.String
e.c.p.code[emitArgsRestMark] = createArgsRestStash
}
} else {
enter = &enterFuncStashless{
efl := &enterFuncStashless{
stackSize: uint32(stackSize),
args: uint32(paramsCount),
}
// Populate dbgNames so the debugger can see stack-register variables.
if e.c.debugMode && (stackSize > 0 || paramsCount > 0) {
efl.dbgNames = s.makeDebugRegisterNamesMap(false)
}
enter = efl
if enterFunc2Mark != -1 {
ef2 := &enterFuncBody{
extensible: e.c.scope.dynamic,
Expand Down
21 changes: 21 additions & 0 deletions compiler_stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func (c *compiler) compileStatement(v ast.Statement, needResult bool) {
case *ast.WithStatement:
c.compileWithStatement(v, needResult)
case *ast.DebuggerStatement:
c.addSrcMap(v)
c.emit(debuggerInstr{})
default:
c.assert(false, int(v.Idx0())-1, "Unknown statement type: %T", v)
panic("unreachable")
Expand Down Expand Up @@ -100,6 +102,25 @@ func (c *compiler) updateEnterBlock(enter *enterBlock) {
}
}
enter.stashSize, enter.stackSize = uint32(stashSize), uint32(stackSize)

if c.debugMode && stashSize > 0 && !scope.dynLookup {
enter.names = scope.makeDebugStashNamesMap()
}

// Build debug names map for stack-register variables so the debugger
// can enumerate and eval let/const variables that aren't in stash.
if c.debugMode && stackSize > 0 && !scope.dynLookup {
idx := 0
for _, b := range scope.bindings {
if !b.inStash {
if enter.dbgNames == nil {
enter.dbgNames = make(map[unistring.String]int, stackSize)
}
enter.dbgNames[b.name] = idx
idx++
}
}
}
}

func (c *compiler) compileTryStatement(v *ast.TryStatement, needResult bool) {
Expand Down
Loading