Skip to content

Commit 29e2772

Browse files
akoclaude
andcommitted
feat: add forward-reference hints when exec fails on later-defined objects
When a statement fails because it references an object defined later in the same script, the error now includes a hint suggesting reordering: hint: Module.NewEdit is defined later in this script — move its CREATE statement before this one ExecuteProgram does a first pass to collect all defined names, then tracks which have been created so far. On error, it checks if any not-yet-created name appears in the message. Closes #53 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fd09fa6 commit 29e2772

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

mdl/executor/executor.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,10 +413,18 @@ func (e *Executor) executeInner(stmt ast.Statement) error {
413413

414414
// ExecuteProgram runs all statements in a program.
415415
func (e *Executor) ExecuteProgram(prog *ast.Program) error {
416+
// Collect all names defined in the script for forward-reference hints.
417+
allDefined := newScriptContext()
418+
allDefined.collectDefinitions(prog)
419+
420+
// Track which names have been created so far.
421+
created := newScriptContext()
422+
416423
for _, stmt := range prog.Statements {
417424
if err := e.Execute(stmt); err != nil {
418-
return err
425+
return annotateForwardRef(err, stmt, created, allDefined)
419426
}
427+
created.collectSingle(stmt)
420428
}
421429
return e.finalizeProgramExecution()
422430
}

mdl/executor/validate.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,85 @@ func (sc *scriptContext) collectDefinitions(prog *ast.Program) {
7070
}
7171
}
7272

73+
// collectSingle records the object defined by a single statement.
74+
func (sc *scriptContext) collectSingle(stmt ast.Statement) {
75+
switch s := stmt.(type) {
76+
case *ast.CreateModuleStmt:
77+
sc.modules[s.Name] = true
78+
case *ast.CreateEntityStmt:
79+
if s.Name.Module != "" {
80+
sc.entities[s.Name.String()] = true
81+
}
82+
case *ast.CreateViewEntityStmt:
83+
if s.Name.Module != "" {
84+
sc.entities[s.Name.String()] = true
85+
}
86+
case *ast.CreateExternalEntityStmt:
87+
if s.Name.Module != "" {
88+
sc.entities[s.Name.String()] = true
89+
}
90+
case *ast.CreateEnumerationStmt:
91+
if s.Name.Module != "" {
92+
sc.enumerations[s.Name.String()] = true
93+
}
94+
case *ast.CreateMicroflowStmt:
95+
if s.Name.Module != "" {
96+
sc.microflows[s.Name.String()] = true
97+
}
98+
case *ast.CreatePageStmtV3:
99+
if s.Name.Module != "" {
100+
sc.pages[s.Name.String()] = true
101+
}
102+
case *ast.CreateSnippetStmtV3:
103+
if s.Name.Module != "" {
104+
sc.snippets[s.Name.String()] = true
105+
}
106+
}
107+
}
108+
109+
// allNames returns all defined names across all categories.
110+
func (sc *scriptContext) allNames() []string {
111+
var names []string
112+
for n := range sc.entities {
113+
names = append(names, n)
114+
}
115+
for n := range sc.enumerations {
116+
names = append(names, n)
117+
}
118+
for n := range sc.microflows {
119+
names = append(names, n)
120+
}
121+
for n := range sc.pages {
122+
names = append(names, n)
123+
}
124+
for n := range sc.snippets {
125+
names = append(names, n)
126+
}
127+
return names
128+
}
129+
130+
// annotateForwardRef checks if a failed statement's error references an object
131+
// that is defined later in the script. If so, it appends a hint to reorder.
132+
func annotateForwardRef(err error, _ ast.Statement, created, allDefined *scriptContext) error {
133+
msg := err.Error()
134+
// Check each name that is defined in the script but not yet created.
135+
for _, name := range allDefined.allNames() {
136+
if created.has(name) {
137+
continue // already created before this statement
138+
}
139+
if strings.Contains(msg, name) {
140+
return fmt.Errorf("%w\n hint: %s is defined later in this script — move its CREATE statement before this one", err, name)
141+
}
142+
}
143+
return err
144+
}
145+
146+
// has returns true if the name exists in any category.
147+
func (sc *scriptContext) has(name string) bool {
148+
return sc.modules[name] || sc.entities[name] || sc.enumerations[name] ||
149+
sc.microflows[name] || sc.pages[name] || sc.snippets[name]
150+
}
151+
73152
// ValidateProgram validates all statements in a program, skipping references
74153
// to objects that are defined within the script itself.
75154
func (e *Executor) ValidateProgram(prog *ast.Program) []error {

0 commit comments

Comments
 (0)