@@ -83,9 +83,237 @@ func Build(ast *Schema) (*metamodel.Schema, error) {
8383 }
8484 }
8585
86+ // Validate the built schema
87+ if err := validate (ast , s ); err != nil {
88+ return nil , err
89+ }
90+
8691 return s , nil
8792}
8893
94+ // validate checks the DSL AST and built schema for common errors.
95+ func validate (ast * Schema , s * metamodel.Schema ) error {
96+ // Build lookup tables
97+ registers := make (map [string ]Register )
98+ for _ , r := range ast .Registers {
99+ registers [r .Name ] = r
100+ }
101+
102+ fnVars := make (map [string ]map [string ]bool ) // fn name → var names
103+ for _ , fn := range ast .Functions {
104+ vars := make (map [string ]bool )
105+ for _ , v := range fn .Vars {
106+ vars [v .Name ] = true
107+ }
108+ fnVars [fn .Name ] = vars
109+ }
110+
111+ // 1. Duplicate detection
112+ {
113+ seen := make (map [string ]string ) // name → kind
114+ for _ , r := range ast .Registers {
115+ if prev , ok := seen [r .Name ]; ok {
116+ return fmt .Errorf ("duplicate name %q (already declared as %s)" , r .Name , prev )
117+ }
118+ seen [r .Name ] = "register"
119+ }
120+ for _ , e := range ast .Events {
121+ if prev , ok := seen [e .Name ]; ok {
122+ return fmt .Errorf ("duplicate name %q (already declared as %s)" , e .Name , prev )
123+ }
124+ seen [e .Name ] = "event"
125+ }
126+ for _ , f := range ast .Functions {
127+ if prev , ok := seen [f .Name ]; ok {
128+ return fmt .Errorf ("duplicate name %q (already declared as %s)" , f .Name , prev )
129+ }
130+ seen [f .Name ] = "function"
131+ }
132+ }
133+
134+ // 2. Reserved name checking
135+ if err := checkReservedName (ast .Name , "schema" ); err != nil {
136+ return err
137+ }
138+ for _ , r := range ast .Registers {
139+ if err := checkReservedName (r .Name , "register" ); err != nil {
140+ return err
141+ }
142+ }
143+ for _ , f := range ast .Functions {
144+ if err := checkReservedName (f .Name , "function" ); err != nil {
145+ return err
146+ }
147+ }
148+
149+ // 3. Arc type checking
150+ for _ , fn := range ast .Functions {
151+ for _ , arc := range fn .Arcs {
152+ // Determine the place side (not the function side)
153+ placeName := arc .Source
154+ indices := arc .SourceIndices
155+ if arc .Source == fn .Name {
156+ placeName = arc .Target
157+ indices = arc .TargetIndices
158+ }
159+
160+ reg , ok := registers [placeName ]
161+ if ! ok {
162+ // Arc references a non-existent register
163+ return fmt .Errorf ("function %s: arc references unknown register %q" , fn .Name , placeName )
164+ }
165+
166+ mapDepth := mapKeyDepth (reg .Type )
167+ indexCount := len (indices )
168+
169+ if mapDepth == 0 && indexCount > 0 {
170+ return fmt .Errorf ("function %s: register %s is %s (scalar), cannot index with [%s]" ,
171+ fn .Name , reg .Name , reg .Type , strings .Join (indices , "][" ))
172+ }
173+
174+ if indexCount > 0 && indexCount != mapDepth {
175+ return fmt .Errorf ("function %s: register %s needs %d index key(s) (type %s), got %d" ,
176+ fn .Name , reg .Name , mapDepth , reg .Type , indexCount )
177+ }
178+ }
179+ }
180+
181+ // 4. Guard variable validation
182+ for _ , fn := range ast .Functions {
183+ if fn .Require == "" {
184+ continue
185+ }
186+ vars := fnVars [fn .Name ]
187+ if err := validateGuardIdents (fn .Name , fn .Require , registers , vars ); err != nil {
188+ return err
189+ }
190+ }
191+
192+ // 5. Event reference validation
193+ eventNames := make (map [string ]bool )
194+ for _ , e := range ast .Events {
195+ eventNames [e .Name ] = true
196+ }
197+ for _ , fn := range ast .Functions {
198+ if fn .EventRef != "" && ! eventNames [fn .EventRef ] {
199+ return fmt .Errorf ("function %s: @event references undeclared event %q" , fn .Name , fn .EventRef )
200+ }
201+ }
202+
203+ return nil
204+ }
205+
206+ // checkReservedName returns an error if the name conflicts with Solidity or Foundry.
207+ func checkReservedName (name , kind string ) error {
208+ reserved := map [string ]string {
209+ // Solidity keywords
210+ "function" : "Solidity keyword" , "event" : "Solidity keyword" ,
211+ "mapping" : "Solidity keyword" , "constructor" : "Solidity keyword" ,
212+ "require" : "Solidity keyword" , "revert" : "Solidity keyword" ,
213+ "assert" : "Solidity keyword" , "return" : "Solidity keyword" ,
214+ "if" : "Solidity keyword" , "else" : "Solidity keyword" ,
215+ "for" : "Solidity keyword" , "while" : "Solidity keyword" ,
216+ "true" : "Solidity keyword" , "false" : "Solidity keyword" ,
217+ // Solidity built-in variables
218+ "msg" : "Solidity built-in" , "block" : "Solidity built-in" ,
219+ "tx" : "Solidity built-in" , "this" : "Solidity built-in" ,
220+ // Forge-std conflicts
221+ "Test" : "forge-std class" , "Script" : "forge-std class" ,
222+ "Vm" : "forge-std class" , "console" : "forge-std class" ,
223+ // Generated contract internals
224+ "contractOwner" : "generated internal" , "currentEpoch" : "generated internal" ,
225+ "eventSequence" : "generated internal" ,
226+ }
227+ if reason , ok := reserved [name ]; ok {
228+ return fmt .Errorf ("%s name %q conflicts with %s" , kind , name , reason )
229+ }
230+ return nil
231+ }
232+
233+ // mapKeyDepth returns the nesting depth of a map type.
234+ // "uint256" → 0, "map[address]uint256" → 1, "map[address]map[address]uint256" → 2
235+ func mapKeyDepth (typ string ) int {
236+ depth := 0
237+ remaining := typ
238+ for strings .HasPrefix (remaining , "map[" ) {
239+ close := strings .Index (remaining , "]" )
240+ if close == - 1 {
241+ break
242+ }
243+ depth ++
244+ remaining = remaining [close + 1 :]
245+ }
246+ return depth
247+ }
248+
249+ // validateGuardIdents checks that all identifiers in a guard expression
250+ // are known registers, function variables, or built-in names.
251+ func validateGuardIdents (fnName , guard string , registers map [string ]Register , vars map [string ]bool ) error {
252+ // Extract identifiers from the guard using simple scanning
253+ // (avoid importing guard package to prevent circular deps)
254+ idents := extractIdents (guard )
255+
256+ builtins := map [string ]bool {
257+ "caller" : true , "address" : true , "true" : true , "false" : true ,
258+ "msg" : true , "sender" : true , // for msg.sender
259+ }
260+
261+ for _ , id := range idents {
262+ if builtins [id ] {
263+ continue
264+ }
265+ if _ , ok := registers [id ]; ok {
266+ continue
267+ }
268+ if vars [id ] {
269+ continue
270+ }
271+ // Allow numeric literals
272+ if isNumeric (id ) {
273+ continue
274+ }
275+ // Allow known function-like calls (vestedAmount, etc.)
276+ // These are generated helper functions, not user-defined
277+ if strings .Contains (guard , id + "(" ) {
278+ continue
279+ }
280+ return fmt .Errorf ("function %s: guard references unknown identifier %q" , fnName , id )
281+ }
282+ return nil
283+ }
284+
285+ // extractIdents returns all identifier-like tokens from an expression string.
286+ func extractIdents (expr string ) []string {
287+ var idents []string
288+ i := 0
289+ for i < len (expr ) {
290+ if isLetter (expr [i ]) || expr [i ] == '_' {
291+ start := i
292+ for i < len (expr ) && (isLetter (expr [i ]) || isDigit (expr [i ]) || expr [i ] == '_' || expr [i ] == '.' ) {
293+ i ++
294+ }
295+ idents = append (idents , expr [start :i ])
296+ } else {
297+ i ++
298+ }
299+ }
300+ return idents
301+ }
302+
303+ func isLetter (c byte ) bool { return (c >= 'a' && c <= 'z' ) || (c >= 'A' && c <= 'Z' ) }
304+ func isDigit (c byte ) bool { return c >= '0' && c <= '9' }
305+ func isNumeric (s string ) bool {
306+ if len (s ) == 0 {
307+ return false
308+ }
309+ for _ , c := range s {
310+ if c < '0' || c > '9' {
311+ return false
312+ }
313+ }
314+ return true
315+ }
316+
89317// buildArc converts a DSL Arc into a metamodel.Arc.
90318// The function name determines direction:
91319// - PLACE -|w|> fnName => input arc (Source=PLACE, Target=fnName)
0 commit comments