Skip to content

Commit 4e67e7e

Browse files
refactor: 实现模块导入沙箱化并重构 OpImportInit 执行流
- **核心重构**: - **重构 `OpImportInit`**:废弃通过直接切换父 `session` 字段来“劫持”执行流的旧模式,改为在独立的模块上下文中同步执行导入程序。 - **引入独立 Module Session**:模块初始化现在拥有独立的 `module session`,父 session 仅在初始化成功后接收产物(Module Value)并同步 `StepCount` 与 `ModuleCache` 状态。 - **显式 Commit/Rollback 机制**:模块仅在执行成功后才会被写入 `ModuleCache`;若初始化失败(如触发除零异常),会自动从 `LoadingModules` 中移除,确保父 session 不被半初始化状态污染。 - **执行器增强**: - 新增 `executeImportedProgram` 与 `buildImportedModuleValue` 私有方法,封装模块执行与导出符号封装逻辑。 - 统一模块成员(全局变量、函数、常量、结构体)的导出过滤规则(首字母大写)。 - **健壮性与测试**: - **新增 E2E 测试**:增加 `module_isolation_test.go`,验证模块初始化失败时不会污染父 session 的缓存与状态。 - **文档**:更新 `TODO.md`,标志着“模块加载沙箱化”核心任务(重构 `OpImportInit`、引入独立 session 及回滚机制)已完成,并标注了关于 `panic` 语义建模的后续计划。
1 parent 1a714f7 commit 4e67e7e

3 files changed

Lines changed: 146 additions & 126 deletions

File tree

TODO.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,11 @@
125125
**目标**: 让模块加载、编译产物、执行 IR 真正成为可隔离、可缓存、可序列化的工业化管线。**
126126

127127
### Q. 模块加载沙箱化
128-
- [ ] **重构 `OpImportInit`**: 不再通过直接切换当前 session/executor 字段来“劫持”执行流
129-
- [ ] **引入独立 module session**: 模块初始化在隔离上下文执行
130-
- [ ] **引入显式 commit/rollback 机制**: 模块初始化失败时主 session 状态不被污染
128+
- [x] **重构 `OpImportInit`**: import 主路径已改为在独立模块上下文中同步执行并返回模块值,不再直接切换父 `session.Executor/Stack/ValueStack/LHSStack` 劫持执行流
129+
- [x] **引入独立 module session**: 脚本模块初始化已在独立 `module session` 中执行,父 session 仅接收成功产物与 step/module 状态回写
130+
- [x] **引入显式 commit/rollback 机制**: 模块初始化仅在成功后写入 `ModuleCache`;失败路径会回滚 `LoadingModules`,并保持父 session/module cache 不被半初始化状态污染
131131
- [ ] **补充循环依赖、panic、部分初始化场景测试**
132+
- [ ] **补记语法约束缺口:`panic` 目前未作为“结束结构”参与语义建模**。这会影响“模块初始化失败即终止/回滚”的静态表达能力;后续需要在语法/语义层明确 `panic` 的终止属性,再回头收紧 import/init failure 与 unreachable/partial-init 相关校验。
132133

133134
### R. IR/Bytecode 管线统一
134135
- [ ] **统一 `core/runtime/task.go``core/compiler/bytecode.go` 的指令集定义**

core/e2e/module_isolation_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
engine "gopkg.d7z.net/go-mini/core"
10+
"gopkg.d7z.net/go-mini/core/ast"
11+
"gopkg.d7z.net/go-mini/core/ffigo"
12+
)
13+
14+
func TestModuleInitFailureDoesNotPolluteParentSession(t *testing.T) {
15+
executor := engine.NewMiniExecutor()
16+
17+
executor.SetLoader(func(path string) (*ast.ProgramStmt, error) {
18+
switch path {
19+
case "broken":
20+
code := `
21+
package broken
22+
23+
var Exported = "partial"
24+
var Trigger = 1 / 0
25+
`
26+
converter := ffigo.NewGoToASTConverter()
27+
node, err := converter.ConvertSource("snippet", code)
28+
if err != nil {
29+
return nil, err
30+
}
31+
return node.(*ast.ProgramStmt), nil
32+
default:
33+
return nil, fmt.Errorf("module not found: %s", path)
34+
}
35+
})
36+
37+
runtime, err := executor.NewRuntimeByGoCode(`
38+
package main
39+
import "broken"
40+
41+
func main() {}
42+
`)
43+
if err != nil {
44+
t.Fatalf("compile failed: %v", err)
45+
}
46+
47+
err = runtime.Execute(context.Background())
48+
if err == nil {
49+
t.Fatal("expected broken module init to fail")
50+
}
51+
if !strings.Contains(err.Error(), "division by zero") {
52+
t.Fatalf("unexpected execute error: %v", err)
53+
}
54+
55+
session := runtime.LastSession()
56+
if session == nil {
57+
t.Fatal("expected last session")
58+
}
59+
if _, ok := session.ModuleCache["broken"]; ok {
60+
t.Fatalf("broken module should not be committed into cache: %#v", session.ModuleCache["broken"])
61+
}
62+
if session.LoadingModules["broken"] {
63+
t.Fatal("broken module should not remain in loading set")
64+
}
65+
}

core/runtime/executor.go

Lines changed: 77 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,65 +1870,13 @@ func (e *Executor) dispatch(session *StackContext, task Task) error {
18701870
prog, err := e.Loader(path)
18711871
if err == nil {
18721872
session.LoadingModules[path] = true
1873-
modExecutor, err := NewExecutor(prog)
1873+
res, err := e.executeImportedProgram(session, path, prog)
1874+
delete(session.LoadingModules, path)
18741875
if err != nil {
1875-
delete(session.LoadingModules, path)
18761876
return err
18771877
}
1878-
modExecutor.Loader = e.Loader
1879-
modExecutor.routes = e.routes
1880-
1881-
modSession := &StackContext{
1882-
Context: session.Context,
1883-
Executor: modExecutor,
1884-
Stack: &Stack{MemoryPtr: make(map[string]*Var), Globals: make(map[string]*Var), Frame: &SlotFrame{}, Scope: "global", Depth: 1},
1885-
StepLimit: session.StepLimit,
1886-
StepCount: session.StepCount,
1887-
ModuleCache: session.ModuleCache,
1888-
LoadingModules: session.LoadingModules,
1889-
Debugger: session.Debugger,
1890-
ValueStack: &ValueStack{},
1891-
LHSStack: &LHSStack{},
1892-
}
1893-
1894-
// Push Done task to current stack (restore context later)
1895-
session.TaskStack = append(session.TaskStack, Task{
1896-
Op: OpImportDone,
1897-
Data: &ImportData{
1898-
Path: path,
1899-
OldExecutor: session.Executor,
1900-
OldStack: session.Stack,
1901-
OldTaskStack: session.TaskStack,
1902-
OldValueStack: session.ValueStack,
1903-
OldLHSStack: session.LHSStack,
1904-
ModSession: modSession,
1905-
},
1906-
})
1907-
1908-
// Switch current session fields
1909-
session.Executor = modExecutor
1910-
session.Stack = modSession.Stack
1911-
session.ValueStack = modSession.ValueStack
1912-
session.LHSStack = modSession.LHSStack
1913-
session.UnwindMode = UnwindNone
1914-
1915-
// Push Global variables init
1916-
for i := len(modExecutor.globalInitOrder) - 1; i >= 0; i-- {
1917-
name := modExecutor.globalInitOrder[i]
1918-
global, ok := modExecutor.lookupGlobal(name)
1919-
if !ok {
1920-
continue
1921-
}
1922-
if global == nil || !global.HasInit {
1923-
continue
1924-
}
1925-
session.TaskStack = append(session.TaskStack, Task{Op: OpInitVar, Data: string(name)})
1926-
session.TaskStack = append(session.TaskStack, cloneTasks(global.InitPlan)...)
1927-
}
1928-
1929-
// Push Main block execution
1930-
session.TaskStack = append(session.TaskStack, cloneTasks(modExecutor.mainTasks)...)
1931-
1878+
session.ModuleCache[path] = res
1879+
session.ValueStack.Push(res)
19321880
return nil
19331881
}
19341882
}
@@ -1977,73 +1925,7 @@ func (e *Executor) dispatch(session *StackContext, task Task) error {
19771925
return fmt.Errorf("failed to load module %s", path)
19781926

19791927
case OpImportDone:
1980-
data := task.Data.(*ImportData)
1981-
path := data.Path
1982-
modSession := data.ModSession
1983-
modExec := modSession.Executor.(*Executor)
1984-
1985-
delete(session.LoadingModules, path)
1986-
1987-
exports := make(map[string]*Var)
1988-
for name := range modExec.globals {
1989-
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
1990-
v, err := modSession.Load(string(name))
1991-
if err == nil {
1992-
exports[string(name)] = v
1993-
}
1994-
}
1995-
}
1996-
for name, fn := range modExec.functions {
1997-
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
1998-
exports[string(name)] = &Var{
1999-
VType: TypeClosure,
2000-
Ref: &VMClosure{
2001-
FunctionType: fn.FunctionType,
2002-
BodyTasks: cloneTasks(fn.BodyTasks),
2003-
UpvalueSlots: nil,
2004-
UpvalueNames: nil,
2005-
Context: modSession,
2006-
},
2007-
}
2008-
}
2009-
}
2010-
for name, val := range modExec.consts {
2011-
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
2012-
exports[name] = NewString(val)
2013-
}
2014-
}
2015-
for name, s := range modExec.metadata.structsByName {
2016-
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
2017-
exports[string(name)] = &Var{
2018-
VType: TypeAny,
2019-
Ref: cloneRuntimeStructSpec(s),
2020-
}
2021-
}
2022-
}
2023-
2024-
// modSession.Stack should remain its own global stack for future member access to module variables
2025-
// modSession.Stack = session.Stack <--- REMOVED THIS LINE
2026-
2027-
// Restore session
2028-
session.Executor = data.OldExecutor.(ExecutorAPI)
2029-
session.Stack = data.OldStack
2030-
session.TaskStack = data.OldTaskStack
2031-
session.ValueStack = data.OldValueStack
2032-
session.LHSStack = data.OldLHSStack
2033-
session.UnwindMode = UnwindNone
2034-
2035-
res := &Var{
2036-
VType: TypeModule,
2037-
Ref: &VMModule{
2038-
Name: path,
2039-
Data: exports,
2040-
Context: modSession,
2041-
},
2042-
}
2043-
2044-
session.ModuleCache[path] = res
2045-
session.ValueStack.Push(res)
2046-
return nil
1928+
return fmt.Errorf("OpImportDone should not be reached in synchronous import mode")
20471929
case OpPush:
20481930
if v, ok := task.Data.(*Var); ok {
20491931
session.ValueStack.Push(v)
@@ -2711,6 +2593,78 @@ func (e *Executor) GetProgram() *ast.ProgramStmt {
27112593
return e.program
27122594
}
27132595

2596+
func (e *Executor) buildImportedModuleValue(path string, modExec *Executor, modSession *StackContext) *Var {
2597+
exports := make(map[string]*Var)
2598+
for name := range modExec.globals {
2599+
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
2600+
v, err := modSession.Load(string(name))
2601+
if err == nil {
2602+
exports[string(name)] = v
2603+
}
2604+
}
2605+
}
2606+
for name, fn := range modExec.functions {
2607+
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
2608+
exports[string(name)] = &Var{
2609+
VType: TypeClosure,
2610+
Ref: &VMClosure{
2611+
FunctionType: fn.FunctionType,
2612+
BodyTasks: cloneTasks(fn.BodyTasks),
2613+
UpvalueSlots: nil,
2614+
UpvalueNames: nil,
2615+
Context: modSession,
2616+
},
2617+
}
2618+
}
2619+
}
2620+
for name, val := range modExec.consts {
2621+
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
2622+
exports[name] = NewString(val)
2623+
}
2624+
}
2625+
for name, s := range modExec.metadata.structsByName {
2626+
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
2627+
exports[string(name)] = &Var{
2628+
VType: TypeAny,
2629+
Ref: cloneRuntimeStructSpec(s),
2630+
}
2631+
}
2632+
}
2633+
2634+
return &Var{
2635+
VType: TypeModule,
2636+
Ref: &VMModule{
2637+
Name: path,
2638+
Data: exports,
2639+
Context: modSession,
2640+
},
2641+
}
2642+
}
2643+
2644+
func (e *Executor) executeImportedProgram(parent *StackContext, path string, prog *ast.ProgramStmt) (*Var, error) {
2645+
modExecutor, err := NewExecutor(prog)
2646+
if err != nil {
2647+
return nil, err
2648+
}
2649+
modExecutor.Loader = e.Loader
2650+
modExecutor.StepLimit = e.StepLimit
2651+
modExecutor.routes = e.routes
2652+
2653+
modSession := modExecutor.NewSession(parent.Context, "global")
2654+
modSession.StepLimit = parent.StepLimit
2655+
modSession.StepCount = parent.StepCount
2656+
modSession.ModuleCache = parent.ModuleCache
2657+
modSession.LoadingModules = parent.LoadingModules
2658+
modSession.Debugger = parent.Debugger
2659+
2660+
if err := modExecutor.InitializeSession(modSession, nil, false); err != nil {
2661+
parent.StepCount = modSession.StepCount
2662+
return nil, err
2663+
}
2664+
parent.StepCount = modSession.StepCount
2665+
return e.buildImportedModuleValue(path, modExecutor, modSession), nil
2666+
}
2667+
27142668
func (e *Executor) ExecuteStmts(session *StackContext, stmts []ast.Stmt) error {
27152669
oldTasks := session.TaskStack
27162670
oldValues := session.ValueStack

0 commit comments

Comments
 (0)