View Javadoc
1   /*
2    * Copyright 2019-2022 Foreseeti AB <https://foreseeti.com>
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.mal_lang.lib;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.LinkedHashMap;
24  import java.util.LinkedHashSet;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.Map;
29  import java.util.Set;
30  
31  public class Analyzer {
32    private MalLogger LOGGER;
33    private Map<String, AST.Asset> assets = new LinkedHashMap<>();
34    private Map<String, Scope<AST.Variable>> assetVariables = new LinkedHashMap<>();
35    private Map<String, Scope<AST.Association>> fields = new LinkedHashMap<>();
36    private Map<String, Scope<AST.AttackStep>> steps = new LinkedHashMap<>();
37    private Set<AST.Variable> currentVariables = new LinkedHashSet<>();
38    private Map<AST.Variable, Integer> variableReferenceCount = new HashMap<>();
39    private Map<AST.Association, Map<String, Integer>> fieldReferenceCount = new HashMap<>();
40  
41    private AST ast;
42    private boolean failed;
43  
44    private Analyzer(AST ast, boolean verbose, boolean debug) {
45      Locale.setDefault(Locale.ROOT);
46      LOGGER = new MalLogger("ANALYZER", verbose, debug);
47      this.ast = ast;
48    }
49  
50    public static void analyze(AST ast) throws CompilerException {
51      analyze(ast, false, false);
52    }
53  
54    public static void analyze(AST ast, boolean verbose, boolean debug) throws CompilerException {
55      new Analyzer(ast, verbose, debug).analyzeLog();
56    }
57  
58    private void analyzeLog() throws CompilerException {
59      try {
60        _analyze();
61        LOGGER.print();
62      } catch (CompilerException e) {
63        LOGGER.print();
64        throw e;
65      }
66    }
67  
68    private void _analyze() throws CompilerException {
69      collectAssociations();
70  
71      checkDefines();
72      checkCategories();
73      checkAssets();
74      checkMetas();
75      checkExtends(); // might throw
76  
77      checkAbstract();
78      checkParents(); // might throw
79  
80      checkSteps();
81      checkCIA();
82      checkTTC();
83      checkFields();
84      checkVariables();
85      checkReaches(); // might throw
86  
87      checkAssociations(); // might throw
88  
89      checkUnused();
90  
91      if (failed) {
92        throw exception();
93      }
94    }
95  
96    private void collectAssociations() {
97      for (AST.Association assoc : ast.getAssociations()) {
98        setupFieldReferenceCounts(assoc);
99      }
100   }
101 
102   private void addVariableReference(AST.Variable variable) {
103     int oldval = variableReferenceCount.get(variable);
104     variableReferenceCount.put(variable, oldval + 1);
105   }
106 
107   private void setupFieldReferenceCounts(AST.Association assoc) {
108     Map<String, Integer> fieldCounts = new HashMap<>();
109     fieldCounts.put(assoc.leftField.id, 0);
110     fieldCounts.put(assoc.rightField.id, 0);
111     fieldReferenceCount.put(assoc, fieldCounts);
112   }
113 
114   private void addFieldReference(AST.Association assoc, AST.ID field) {
115     var fieldCounts = fieldReferenceCount.get(assoc);
116     int oldcount = fieldCounts.get(field.id);
117     fieldCounts.put(field.id, oldcount + 1);
118   }
119 
120   private void checkAssociations() throws CompilerException {
121     boolean err = false;
122     for (AST.Association assoc : ast.getAssociations()) {
123       if (!assets.containsKey(assoc.leftAsset.id)) {
124         error(assoc.leftAsset, String.format("Left asset '%s' is not defined", assoc.leftAsset.id));
125         err = true;
126       }
127       if (!assets.containsKey(assoc.rightAsset.id)) {
128         error(
129             assoc.rightAsset,
130             String.format("Right asset '%s' is not defined", assoc.rightAsset.id));
131         err = true;
132       }
133     }
134     if (err) {
135       throw exception();
136     }
137   }
138 
139   private void checkUnused() {
140     // variables
141     for (AST.Variable variable : variableReferenceCount.keySet()) {
142       int val = variableReferenceCount.get(variable);
143       if (val == 0) {
144         LOGGER.warning(
145             variable.name, String.format("Variable '%s' is never used", variable.name.id));
146       }
147     }
148 
149     // fields
150     for (var assoc : fieldReferenceCount.keySet()) {
151       var fieldCounts = fieldReferenceCount.get(assoc);
152       boolean onlyZeroRefs = true;
153       for (var field : fieldCounts.keySet()) {
154         int val = fieldCounts.get(field);
155         if (val > 0) {
156           onlyZeroRefs = false;
157           break;
158         }
159       }
160       if (onlyZeroRefs) {
161         LOGGER.warning(
162             assoc, String.format("Association '%s' is never used", assoc.toShortString()));
163       }
164     }
165   }
166 
167   private void checkDefines() {
168     Map<String, AST.Define> defines = new HashMap<>();
169     for (AST.Define define : ast.getDefines()) {
170       AST.Define prevDef = defines.put(define.key.id, define);
171       if (prevDef != null) {
172         error(
173             define,
174             String.format(
175                 "Define '%s' previously defined at %s", define.key.id, prevDef.posString()));
176       }
177     }
178     AST.Define id = defines.get("id");
179     if (id != null) {
180       if (id.value.isBlank()) {
181         error(id, "Define 'id' cannot be empty");
182       }
183     } else {
184       error("Missing required define '#id: \"\"'");
185     }
186     AST.Define version = defines.get("version");
187     if (version != null) {
188       if (!version.value.matches("\\d+\\.\\d+\\.\\d+")) {
189         error(
190             version,
191             "Define 'version' must be valid semantic versioning without pre-release identifier and"
192                 + " build metadata");
193       }
194     } else {
195       error("Missing required define '#version: \"\"'");
196     }
197   }
198 
199   private void checkCategories() {
200     for (AST.Category category : ast.getCategories()) {
201       if (category.assets.isEmpty() && category.meta.isEmpty()) {
202         LOGGER.warning(
203             category.name,
204             String.format("Category '%s' contains no assets or metadata", category.name.id));
205       }
206     }
207   }
208 
209   private void checkMetas() {
210     // Collect meta info from categories
211     Map<String, List<AST.Meta>> categoryMetas = new HashMap<>();
212     for (var category : ast.getCategories()) {
213       if (!categoryMetas.containsKey(category.name.id)) {
214         categoryMetas.put(category.name.id, new ArrayList<>());
215       }
216       categoryMetas.get(category.name.id).addAll(category.meta);
217     }
218     // Check meta info for categories
219     for (var metas : categoryMetas.values()) {
220       checkMeta(metas);
221     }
222     // Check meta info for assets and attack steps
223     for (var category : ast.getCategories()) {
224       for (var asset : category.assets) {
225         checkMeta(asset.meta);
226         for (var attackStep : asset.attackSteps) {
227           checkMeta(attackStep.meta);
228         }
229       }
230     }
231     // Check meta info for associations
232     for (var association : ast.getAssociations()) {
233       checkMeta(association.meta);
234     }
235   }
236 
237   private void checkMeta(List<AST.Meta> lst) {
238     Map<String, AST.Meta> metas = new HashMap<>();
239     for (var meta : lst) {
240       if (!metas.containsKey(meta.type.id)) {
241         metas.put(meta.type.id, meta);
242       } else {
243         var prevDef = metas.get(meta.type.id);
244         error(
245             meta,
246             String.format(
247                 "Metadata %s previously defined at %s", meta.type.id, prevDef.posString()));
248       }
249     }
250   }
251 
252   private void checkAssets() {
253     for (AST.Category category : ast.getCategories()) {
254       for (AST.Asset asset : category.assets) {
255         if (assets.containsKey(asset.name.id)) {
256           AST.Asset prevDef = assets.get(asset.name.id);
257           error(
258               asset.name,
259               String.format(
260                   "Asset '%s' previously defined at %s", asset.name.id, prevDef.name.posString()));
261         } else {
262           assets.put(asset.name.id, asset);
263         }
264       }
265     }
266   }
267 
268   private void checkExtends() throws CompilerException {
269     boolean err = false;
270     for (AST.Asset asset : assets.values()) {
271       if (asset.parent.isPresent()) {
272         if (getAsset(asset.parent.get()) == null) {
273           err = true;
274         }
275       }
276     }
277     if (err) {
278       throw exception();
279     }
280   }
281 
282   private void checkParents() throws CompilerException {
283     boolean err = false;
284     for (AST.Asset asset : assets.values()) {
285       if (asset.parent.isPresent()) {
286         Set<String> parents = new LinkedHashSet<>();
287         AST.Asset parent = asset;
288         do {
289           if (!parents.add(parent.name.id)) {
290             StringBuilder sb = new StringBuilder();
291             for (String parentName : parents) {
292               sb.append(parentName);
293               sb.append(" -> ");
294             }
295             sb.append(parent.name.id);
296             error(
297                 asset.name,
298                 String.format("Asset '%s' extends in loop '%s'", asset.name.id, sb.toString()));
299             err = true;
300             break;
301           }
302           parent = getAsset(parent.parent.get());
303         } while (parent.parent.isPresent());
304       }
305     }
306     if (err) {
307       throw exception();
308     }
309   }
310 
311   private void checkAbstract() {
312     for (AST.Asset parent : assets.values()) {
313       if (parent.isAbstract) {
314         boolean found = false;
315         for (AST.Asset extendee : assets.values()) {
316           if (extendee.parent.isPresent() && extendee.parent.get().id.equals(parent.name.id)) {
317             found = true;
318             break;
319           }
320         }
321         if (!found) {
322           LOGGER.warning(
323               parent.name,
324               String.format("Asset '%s' is abstract but never extended to", parent.name.id));
325         }
326       }
327     }
328   }
329 
330   private void checkSteps() {
331     for (AST.Asset asset : assets.values()) {
332       Scope<AST.AttackStep> scope = new Scope<>();
333       steps.put(asset.name.id, scope);
334       readSteps(scope, asset);
335     }
336   }
337 
338   private void checkCIA() {
339     for (var asset : assets.values()) {
340       for (var attackStep : asset.attackSteps) {
341         if (attackStep.cia.isPresent()) {
342           if (attackStep.type == AST.AttackStepType.DEFENSE
343               || attackStep.type == AST.AttackStepType.EXIST
344               || attackStep.type == AST.AttackStepType.NOTEXIST) {
345             error(attackStep.name, "Defenses cannot have CIA classifications");
346           }
347           var cias = new HashSet<AST.CIA>();
348           for (var cia : attackStep.cia.get()) {
349             if (cias.contains(cia)) {
350               LOGGER.warning(
351                   attackStep.name,
352                   String.format(
353                       "Attack step %s.%s contains duplicate classification {%s}",
354                       asset.name.id, attackStep.name.id, cia));
355             } else {
356               cias.add(cia);
357             }
358           }
359         }
360       }
361     }
362   }
363 
364   private void checkTTC() {
365     for (AST.Asset asset : assets.values()) {
366       for (AST.AttackStep attackStep : asset.attackSteps) {
367         if (attackStep.ttc.isPresent()) {
368           AST.TTCExpr ttc = attackStep.ttc.get();
369           if (attackStep.type == AST.AttackStepType.DEFENSE) {
370             if (!(ttc instanceof AST.TTCFuncExpr)) {
371               error(
372                   attackStep,
373                   String.format(
374                       "Defense %s.%s may not have advanced TTC expressions",
375                       asset.name.id, attackStep.name.id));
376             } else {
377               AST.TTCFuncExpr func = (AST.TTCFuncExpr) ttc;
378               switch (func.name.id) {
379                 case "Enabled":
380                 case "Disabled":
381                 case "Bernoulli":
382                   try {
383                     Distributions.validate(func.name.id, func.params);
384                   } catch (CompilerException e) {
385                     error(func, e.getMessage());
386                   }
387                   break;
388                 default:
389                   error(
390                       attackStep,
391                       String.format(
392                           "Defense %s.%s may only have 'Enabled', 'Disabled', or 'Bernoulli(p)' as"
393                               + " TTC",
394                           asset.name.id, attackStep.name.id));
395               }
396             }
397           } else if (attackStep.type == AST.AttackStepType.ALL
398               || attackStep.type == AST.AttackStepType.ANY) {
399             checkTTCExpr(attackStep.ttc.get());
400           }
401         }
402       }
403     }
404   }
405 
406   private void checkTTCExpr(AST.TTCExpr expr) {
407     checkTTCExpr(expr, false);
408   }
409 
410   private void checkTTCExpr(AST.TTCExpr expr, boolean isSubDivExp) {
411     if (expr instanceof AST.TTCBinaryExpr) {
412       isSubDivExp =
413           expr instanceof AST.TTCSubExpr
414               || expr instanceof AST.TTCDivExpr
415               || expr instanceof AST.TTCPowExpr;
416       checkTTCExpr(((AST.TTCBinaryExpr) expr).lhs, isSubDivExp);
417       checkTTCExpr(((AST.TTCBinaryExpr) expr).rhs, isSubDivExp);
418     } else if (expr instanceof AST.TTCFuncExpr) {
419       AST.TTCFuncExpr func = (AST.TTCFuncExpr) expr;
420       if (func.name.id.equals("Enabled") || func.name.id.equals("Disabled")) {
421         error(
422             expr,
423             "Distributions 'Enabled' or 'Disabled' may not be used as TTC values in '&' and '|'"
424                 + " attack steps");
425       } else {
426         if (isSubDivExp && Arrays.asList("Bernoulli", "EasyAndUncertain").contains(func.name.id)) {
427           error(
428               expr,
429               String.format(
430                   "TTC distribution '%s' is not available in subtraction, division or exponential"
431                       + " expressions.",
432                   func.name.id));
433         }
434         try {
435           Distributions.validate(func.name.id, func.params);
436         } catch (CompilerException e) {
437           error(func, e.getMessage());
438         }
439       }
440     } else if (expr instanceof AST.TTCNumExpr) {
441       // always ok
442     } else {
443       error(expr, String.format("Unexpected expression '%s'", expr.toString()));
444       System.exit(1);
445     }
446   }
447 
448   /**
449    * Retrieves a list of an assets parents (including itself). The oldest parents will be first in
450    * the list. E.g. Alpha extends Bravo extends Charlie would return [Charlie, Bravo, Alpha] for
451    * asset Alpha.
452    *
453    * @param asset Child asset
454    * @return List of parents, oldest parent first in list
455    */
456   private LinkedList<AST.Asset> getParents(AST.Asset asset) {
457     LinkedList<AST.Asset> lst = new LinkedList<>();
458     lst.addFirst(asset);
459     while (asset.parent.isPresent()) {
460       asset = getAsset(asset.parent.get());
461       lst.addFirst(asset);
462     }
463     return lst;
464   }
465 
466   /**
467    * Populates a scope with attack steps of an asset and its parents. Checks semantic rules to make
468    * sure scope is correctly filled.
469    *
470    * @param scope Scope to populate
471    * @param asset Child asset
472    */
473   private void readSteps(Scope<AST.AttackStep> scope, AST.Asset asset) {
474     List<AST.Asset> parents = getParents(asset);
475     for (AST.Asset parent : parents) {
476       if (parent.parent.isPresent()) {
477         scope = new Scope<>(scope);
478         steps.put(asset.name.id, scope);
479       }
480       for (AST.AttackStep attackStep : parent.attackSteps) {
481         AST.AttackStep prevDef = scope.look(attackStep.name.id);
482         if (prevDef == null) {
483           // Attack step is not defined in current scope
484           prevDef = scope.lookup(attackStep.name.id);
485           if (prevDef == null) {
486             // Attack step is not defined in any scope
487             if (attackStep.reaches.isEmpty() || !attackStep.reaches.get().inherits) {
488               // Attack step either doesn't reach anything or reaches with ->, OK
489               scope.add(attackStep.name.id, attackStep);
490             } else {
491               // Attack step reaches something with +> but doesn't exist previously, NOK
492               error(
493                   attackStep.reaches.get(),
494                   String.format(
495                       "Cannot inherit attack step '%s' without previous definition",
496                       attackStep.name.id));
497             }
498           } else {
499             // Attack step is previously defined in another scope
500             if (attackStep.type.equals(prevDef.type)) {
501               // Step is of same type as previous, OK
502               scope.add(attackStep.name.id, attackStep);
503             } else {
504               // Step is NOT of same type as previous, NOK
505               error(
506                   attackStep.name,
507                   String.format(
508                       "Cannot override attack step '%s' previously defined at %s with different"
509                           + " type '%s' =/= '%s'",
510                       attackStep.name.id, prevDef.name.posString(), attackStep.type, prevDef.type));
511             }
512           }
513         } else {
514           // Attack step is defined in this scope, NOK
515           error(
516               attackStep.name,
517               String.format(
518                   "Attack step '%s' previously defined at %s",
519                   attackStep.name.id, prevDef.name.posString()));
520         }
521       }
522     }
523   }
524 
525   private void checkVariables() {
526     for (AST.Asset asset : assets.values()) {
527       Scope<AST.Variable> scope = new Scope<>();
528       assetVariables.put(asset.name.id, scope);
529       readVariables(scope, asset);
530     }
531 
532     for (AST.Asset asset : assets.values()) {
533       var scope = assetVariables.get(asset.name.id);
534       for (var variable : scope.getSymbols().entrySet()) {
535         variableToAsset(asset, variable.getValue());
536         variableReferenceCount.put(variable.getValue(), 0);
537       }
538     }
539   }
540 
541   /**
542    * Populate a scope with variable names from an asset and its parents
543    *
544    * @param scope
545    * @param asset
546    */
547   private void readVariables(Scope<AST.Variable> scope, AST.Asset asset) {
548     List<AST.Asset> parents = getParents(asset);
549     for (AST.Asset parent : parents) {
550       if (parent.parent.isPresent()) {
551         scope = new Scope<>(scope);
552         assetVariables.put(asset.name.id, scope);
553       }
554       for (AST.Variable variable : parent.variables) {
555         addVariable(scope, variable);
556       }
557     }
558   }
559 
560   private void checkFields() {
561     for (AST.Asset asset : assets.values()) {
562       Scope<AST.Association> scope = new Scope<>();
563       fields.put(asset.name.id, scope);
564       readFields(scope, asset);
565     }
566   }
567 
568   /**
569    * Populates a scope with field names and associations from an asset and its parents.
570    *
571    * @param scope Scope to populate
572    * @param asset Child asset
573    */
574   private void readFields(Scope<AST.Association> scope, AST.Asset asset) {
575     List<AST.Asset> parents = getParents(asset);
576     for (AST.Asset parent : parents) {
577       if (parent.parent.isPresent()) {
578         scope = new Scope<>(scope);
579         fields.put(asset.name.id, scope);
580       }
581       for (AST.Association assoc : ast.getAssociations()) {
582         if (assoc.leftAsset.id.equals(parent.name.id)) {
583           addField(scope, parent, asset, assoc.rightField, assoc);
584         }
585         // Association can be made from one asset to itself
586         if (assoc.rightAsset.id.equals(parent.name.id)) {
587           addField(scope, parent, asset, assoc.leftField, assoc);
588         }
589       }
590     }
591   }
592 
593   private void addField(
594       Scope<AST.Association> scope,
595       AST.Asset parent,
596       AST.Asset asset,
597       AST.ID field,
598       AST.Association assoc) {
599     AST.Association prevDef = scope.lookdown(field.id);
600     if (prevDef == null) {
601       // Field not previously defined
602       AST.ID prevStep = hasStep(asset, field.id);
603       if (prevStep == null) {
604         scope.add(field.id, assoc);
605       } else {
606         // Field previously defined as attack step
607         error(
608             field,
609             String.format(
610                 "Field '%s' previously defined as attack step at %s",
611                 field.id, prevStep.posString()));
612       }
613     } else {
614       // Field previously defined
615       AST.ID prevField;
616       if (field.id.equals(prevDef.rightField.id)) {
617         prevField = prevDef.rightField;
618       } else {
619         prevField = prevDef.leftField;
620       }
621       error(
622           field,
623           String.format(
624               "Field %s.%s previously defined for asset at %s",
625               parent.name.id, field.id, prevField.posString()));
626     }
627   }
628 
629   private void addVariable(Scope<AST.Variable> scope, AST.Variable variable) {
630     AST.Variable prevDef = scope.lookup(variable.name.id);
631     if (prevDef == null) {
632       variableReferenceCount.put(variable, 0);
633       scope.add(variable.name.id, variable);
634     } else {
635       error(
636           variable.name,
637           String.format(
638               "Variable '%s' previously defined at %s",
639               variable.name.id, prevDef.name.posString()));
640     }
641   }
642 
643   /** Evaluates each expression reached by an attack step. */
644   private void checkReaches() throws CompilerException {
645     for (AST.Asset asset : assets.values()) {
646       for (AST.AttackStep attackStep : asset.attackSteps) {
647         if (attackStep.type == AST.AttackStepType.EXIST
648             || attackStep.type == AST.AttackStepType.NOTEXIST) {
649           if (attackStep.ttc.isPresent()) {
650             error(
651                 attackStep,
652                 String.format("Attack step of type '%s' must not have TTC", attackStep.type));
653             continue;
654           }
655           if (attackStep.requires.isPresent()) {
656             // Requires (<-)
657             for (AST.Expr expr : attackStep.requires.get().requires) {
658               // Requires only have expressions that ends in assets/fields, not attack steps.
659               checkToAsset(asset, expr);
660             }
661           } else {
662             error(
663                 attackStep,
664                 String.format("Attack step of type '%s' must have require '<-'", attackStep.type));
665             continue;
666           }
667         } else if (attackStep.requires.isPresent()) {
668           error(
669               attackStep.requires.get(),
670               "Require '<-' may only be defined for attack step type exist 'E' or not-exist '!E'");
671           continue;
672         }
673 
674         if (attackStep.reaches.isPresent()) {
675           for (AST.Expr expr : attackStep.reaches.get().reaches) {
676             checkToStep(asset, expr);
677           }
678         }
679       }
680     }
681     if (failed) {
682       throw exception();
683     }
684   }
685 
686   private AST.AttackStep checkToStep(AST.Asset asset, AST.Expr expr) {
687     if (expr instanceof AST.IDExpr) {
688       AST.IDExpr step = (AST.IDExpr) expr;
689       AST.Asset target = asset;
690       AST.AttackStep attackStep = steps.get(target.name.id).lookup(step.id.id);
691       if (attackStep != null) {
692         return attackStep;
693       } else {
694         error(
695             step.id,
696             String.format(
697                 "Attack step '%s' not defined for asset '%s'", step.id.id, target.name.id));
698         return null;
699       }
700     } else if (expr instanceof AST.StepExpr) {
701       AST.StepExpr step = (AST.StepExpr) expr;
702       AST.Asset target = checkToAsset(asset, step.lhs);
703       if (target != null) {
704         return checkToStep(target, step.rhs);
705       } else {
706         return null;
707       }
708     } else {
709       error(expr, "Last step is not attack step");
710       return null;
711     }
712   }
713 
714   private AST.Asset checkToAsset(AST.Asset asset, AST.Expr expr) {
715     if (expr instanceof AST.StepExpr) {
716       return checkStepExpr(asset, (AST.StepExpr) expr);
717     } else if (expr instanceof AST.IDExpr) {
718       return checkIDExpr(asset, (AST.IDExpr) expr);
719     } else if (expr instanceof AST.IntersectionExpr
720         || expr instanceof AST.UnionExpr
721         || expr instanceof AST.DifferenceExpr) {
722       return checkSetExpr(asset, (AST.BinaryExpr) expr);
723     } else if (expr instanceof AST.TransitiveExpr) {
724       return checkTransitiveExpr(asset, (AST.TransitiveExpr) expr);
725     } else if (expr instanceof AST.SubTypeExpr) {
726       return checkSubTypeExpr(asset, (AST.SubTypeExpr) expr);
727     } else if (expr instanceof AST.CallExpr) {
728       return checkCallExpr(asset, (AST.CallExpr) expr);
729     } else {
730       error(expr, String.format("Unexpected expression '%s'", expr.toString()));
731       System.exit(1);
732       return null;
733     }
734   }
735 
736   private AST.Asset checkStepExpr(AST.Asset asset, AST.StepExpr expr) {
737     AST.Asset leftTarget = checkToAsset(asset, expr.lhs);
738     if (leftTarget != null) {
739       AST.Asset rightTarget = checkToAsset(leftTarget, expr.rhs);
740       return rightTarget;
741     } else {
742       return null;
743     }
744   }
745 
746   /**
747    * When evaluating a variable (variableToAsset()), a record must be kept to check for cyclic usage
748    * of variables.
749    *
750    * @param variable Variable evaluated
751    * @return True if variable is not being evaluated, false otherwise
752    */
753   private boolean evalVariableBegin(AST.Variable variable) {
754     addVariableReference(variable);
755     if (currentVariables.add(variable)) {
756       return true;
757     } else {
758       StringBuilder sb = new StringBuilder();
759       for (var key : currentVariables) {
760         sb.append(key.name.id);
761         sb.append(" -> ");
762       }
763       sb.append(variable.name.id);
764       AST.Variable first = (AST.Variable) currentVariables.toArray()[0];
765       error(
766           first.name,
767           String.format("Variable '%s' contains cycle '%s'", first.name.id, sb.toString()));
768       return false;
769     }
770   }
771 
772   private void evalVariableEnd(AST.Variable variable) {
773     currentVariables.remove(variable);
774   }
775 
776   private AST.Asset variableToAsset(AST.Asset asset, AST.Variable variable) {
777     if (evalVariableBegin(variable)) {
778 
779       AST.Asset res = checkToAsset(asset, variable.expr);
780       evalVariableEnd(variable);
781       return res;
782     } else {
783       return null;
784     }
785   }
786 
787   private AST.Asset checkCallExpr(AST.Asset asset, AST.CallExpr expr) {
788     var scope = assetVariables.get(asset.name.id);
789     var variableScope = scope.getScopeFor(expr.id.id);
790     if (variableScope != null) {
791       var variable = variableScope.look(expr.id.id);
792       if (variable != null) {
793         return variableToAsset(asset, variable);
794       }
795     }
796     error(expr, String.format("Variable '%s' is not defined", expr.id.id));
797     return null;
798   }
799 
800   private AST.Asset checkIDExpr(AST.Asset asset, AST.IDExpr expr) {
801     return getTarget(asset, expr.id);
802   }
803 
804   private AST.Asset checkSetExpr(AST.Asset asset, AST.BinaryExpr expr) {
805     AST.Asset leftTarget = checkToAsset(asset, expr.lhs);
806     AST.Asset rightTarget = checkToAsset(asset, expr.rhs);
807     if (leftTarget == null || rightTarget == null) {
808       return null;
809     }
810     AST.Asset target = getLCA(leftTarget, rightTarget);
811     if (target != null) {
812       return target;
813     } else {
814       error(
815           expr,
816           String.format(
817               "Types '%s' and '%s' have no common ancestor",
818               leftTarget.name.id, rightTarget.name.id));
819       return null;
820     }
821   }
822 
823   private AST.Asset checkTransitiveExpr(AST.Asset asset, AST.TransitiveExpr expr) {
824     AST.Asset res = checkToAsset(asset, expr.e);
825     if (res == null) {
826       return null;
827     }
828     if (isChild(res, asset)) {
829       return res;
830     } else {
831       error(
832           expr,
833           String.format("Previous asset '%s' is not of type '%s'", asset.name.id, res.name.id));
834       return null;
835     }
836   }
837 
838   private AST.Asset checkSubTypeExpr(AST.Asset asset, AST.SubTypeExpr expr) {
839     AST.Asset target = checkToAsset(asset, expr.e);
840     if (target == null) {
841       return null;
842     }
843     AST.Asset type = getAsset(expr.subType);
844     if (type == null) {
845       return null;
846     }
847     if (isChild(target, type)) {
848       return type;
849     } else {
850       error(expr, String.format("Asset '%s' cannot be of type '%s'", target.name.id, type.name.id));
851       return null;
852     }
853   }
854 
855   private AST.Asset getAsset(AST.ID name) {
856     if (assets.containsKey(name.id)) {
857       return assets.get(name.id);
858     } else {
859       error(name, String.format("Asset '%s' not defined", name.id));
860       return null;
861     }
862   }
863 
864   private AST.ID hasStep(AST.Asset asset, String name) {
865     Scope<AST.AttackStep> scope = steps.get(asset.name.id);
866     AST.AttackStep attackStep = scope.lookdown(name);
867     if (attackStep != null) {
868       return attackStep.name;
869     } else {
870       return null;
871     }
872   }
873 
874   private AST.Asset getTarget(AST.Asset asset, AST.ID name) {
875     Scope<AST.Association> scope = fields.get(asset.name.id);
876     AST.Association assoc = scope.lookdown(name.id);
877     if (assoc != null) {
878       addFieldReference(assoc, name);
879       if (assoc.leftField.id.equals(name.id)) {
880         return getAsset(assoc.leftAsset);
881       } else {
882         return getAsset(assoc.rightAsset);
883       }
884     } else {
885       String extra = "";
886       var varScope = assetVariables.get(asset.name.id).lookdown(name.id);
887       if (varScope != null) {
888         extra =
889             String.format(
890                 ", did you mean the variable '%s()' defined at %s", name.id, varScope.posString());
891       }
892       error(
893           name,
894           String.format("Field '%s' not defined for asset '%s'%s", name.id, asset.name.id, extra));
895       return null;
896     }
897   }
898 
899   private boolean isChild(AST.Asset parent, AST.Asset child) {
900     if (parent.name.id.equals(child.name.id)) {
901       return true;
902     } else if (child.parent.isEmpty()) {
903       return false;
904     } else {
905       AST.Asset childParent = getAsset(child.parent.get());
906       return isChild(parent, childParent);
907     }
908   }
909 
910   private AST.Asset getLCA(AST.Asset left, AST.Asset right) {
911     if (isChild(left, right)) {
912       return left;
913     } else if (isChild(right, left)) {
914       return right;
915     } else if (!left.parent.isPresent() && !right.parent.isPresent()) {
916       return null;
917     } else {
918       AST.Asset lparent = getAsset(left.parent.orElse(left.name));
919       AST.Asset rparent = getAsset(right.parent.orElse(right.name));
920       return getLCA(lparent, rparent);
921     }
922   }
923 
924   private CompilerException exception() {
925     return new CompilerException("There were semantic errors");
926   }
927 
928   private void error(String msg) {
929     failed = true;
930     LOGGER.error(msg);
931   }
932 
933   private void error(Position pos, String msg) {
934     failed = true;
935     LOGGER.error(pos, msg);
936   }
937 }