Test Failed
Push — master ( 3b4c09...efb6fb )
by Richard
02:50
created

Compiler::purge_predefined_variables()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 0
1
<?php
2
/******************************************************************************
3
 * An implementation of dicto (scg.unibe.ch/dicto) in and for PHP.
4
 *
5
 * Copyright (c) 2016 Richard Klees <[email protected]>
6
 *
7
 * This software is licensed under The MIT License. You should have received
8
 * a copy of the license along with the code.
9
 */
10
11
namespace Lechimp\Dicto\Definition;
12
13
use Lechimp\Dicto\Rules\Ruleset;
14
use Lechimp\Dicto\Variables as V;
15
use Lechimp\Dicto\Rules as R;
16
17
/**
18
 * Compiles an AST to a ruleset.
19
 */
20
class Compiler implements ArgumentParser {
21
    /**
22
     * @var V\Variable[]
23
     */
24
    protected $predefined_variables;
25
26
    /**
27
     * @var R\Schema[]
28
     */
29
    protected $schemas;
30
31
    /**
32
     * @var array<string,R\Property[]>
33
     */
34
    protected $properties;
35
36
    /**
37
     * @var array<string,V\Variable[]>
38
     */
39
    protected $variables = array();
40
41
    /**
42
     * @var R\Rule[]
43
     */
44
    protected $rules = array();
45
46
    /**
47
     * @var string|null
48
     */
49
    protected $last_explanation = null;
50
51
    /**
52
     * @var Parameter[]|null
53
     */
54
    protected $current_parameters = [];
55
56
    /**
57
     * @param   V\Variable[]    $predefined_variables
58
     * @param   R\Schema[]      $schemas
59
     * @param   V\Property[]    $properties
60
     */
61
    public function __construct( array $predefined_variables
62
                               , array $schemas
63
                               , array $properties) {
64
        $this->predefined_variables = array_map(function(V\Variable $v) {
65
            return $v;
66
        }, $predefined_variables);
67
68
        $this->schemas = [];
69
        foreach ($schemas as $schema) (function(R\Schema $s) {
70
            $this->schemas[$s->name()] = $s;
71
        })($schema);
72
73
        $this->properties = [];
74
        foreach ($properties as $property) (function(V\Property $p) {
75
            $this->properties[$p->parse_as()] = $p;
76
        })($property);
77
    }
78
79
    /**
80
     * Compile an AST to an according entity.
81
     *
82
     * @param   AST\Node    $node
83
     * @return  mixed
84
     */
85
    public function compile(AST\Root $node) {
86
        $this->variables = array();
87
        $this->rules = array();
88
        $this->current_parameters = [];
89
        $this->add_predefined_variables();
90
        return $this->compile_root($node);
91
    }
92
93
    protected function compile_root(AST\Root $node) {
94
        foreach ($node->lines() as $line) {
95
            if ($line instanceof AST\Assignment) {
96
                $this->compile_assignment($line);
97
                $this->last_explanation = null;
98
            }
99
            else if ($line instanceof AST\Rule) {
100
                $this->compile_rule($line);
101
                $this->last_explanation = null;
102
            }
103
            else if ($line instanceof AST\Explanation) {
104
                $this->last_explanation = $line;
0 ignored issues
show
Documentation Bug introduced by
It seems like $line of type object<Lechimp\Dicto\Definition\AST\Explanation> is incompatible with the declared type string|null of property $last_explanation.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
105
            }
106
            else {
107
                throw new \LogicException("Can't compile '".get_class($line)."'");
108
            }
109
        }
110
        $this->purge_predefined_variables();
111
        return new Ruleset($this->variables, $this->rules);
112
    }
113
114
    protected function compile_assignment(AST\Assignment $node) {
115
        $def = $this->compile_definition($node->definition());
116
        if ($this->last_explanation !== null) {
117
            $def = $def->withExplanation($this->last_explanation->content());
0 ignored issues
show
Bug introduced by
The method content cannot be called on $this->last_explanation (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
118
        }
119
        $this->add_variable
120
            ("".$node->name()
121
            , $def
122
            );
123
    }
124
125
    protected function compile_rule(AST\Rule $node) {
126
        $property = $node->definition();
127
        // TODO: This is morally wrong. In general all rules are existence rules
128
        // so I should push the schema into the definition of the targeted variables
129
        // on the interpretation side as well.
130
        if (!($property instanceof AST\Property)) {
131
            throw new \LogicException("Expected definition of rule to be AST\Property");
132
        }
133
        $schema = $this->get_schema("".$property->id());
134
        $rule = new R\Rule
135
            ( $this->compile_qualifier($node->qualifier())
136
            , $this->compile_definition($property->left())
137
            , $schema
138
            , $this->compile_parameters($schema, $property->parameters())
139
            );
140
        if ($this->last_explanation !== null) {
141
            $rule = $rule->withExplanation($this->last_explanation->content());
0 ignored issues
show
Bug introduced by
The method content cannot be called on $this->last_explanation (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
142
        }
143
        $this->rules[] = $rule;
144
    }
145
146
    protected function compile_definition(AST\Definition $node) {
147
        if ($node instanceof AST\Name) {
148
            return $this->get_variable("$node");
149
        }
150
        if ($node instanceof AST\Any) {
151
            return $this->compile_any($node);
152
        }
153
        if ($node instanceof AST\Except) {
154
            return $this->compile_except($node);
155
        }
156
        if ($node instanceof AST\Property) {
157
            return $this->compile_property($node);
158
        }
159
        throw new \LogicException("Can't compile '".get_class($node)."'");
160
    }
161
162
    protected function compile_any(AST\Any $node) {
163
        $definitions = $node->definitions();
164
165
        // short circuit for any with only one element.
166
        // TODO: this should become part of some transformation pass of
167
        // the AST.
168
        if (count($definitions) == 1) {
169
            return $this->compile_definition($definitions[0]);
170
        }
171
172
        return new V\Any
173
            (array_map
174
                ( [$this, "compile_definition"]
175
                , $definitions
176
                )
177
            );
178
    }
179
180
    protected function compile_except(AST\Except $node) {
181
        return new V\Except
182
            ( $this->compile_definition($node->left())
183
            , $this->compile_definition($node->right())
184
            );
185
    }
186
187
    protected function compile_property(AST\Property $node) {
188
        $variable = $this->compile_definition($node->left());
189
        $property = $this->get_property("".$node->id());
190
        $parameters = $this->compile_parameters($property, $node->parameters());
191
        return new V\WithProperty($variable, $property, $parameters);
0 ignored issues
show
Documentation introduced by
$property is of type array<integer,object<Lec...\Dicto\Rules\Property>>, but the function expects a object<Lechimp\Dicto\Variables\Property>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
192
    }
193
194
    protected function compile_parameters($prop_or_schema, array $parameters) {
195
        assert('$prop_or_schema instanceof Lechimp\Dicto\Variable\Property || $prop_or_schema instanceof Lechimp\Dicto\Rules\Rule');
196
        // TODO: rename fetch_arguments to fetch_parameters
197
        $this->current_parameters = $parameters;
198
        $parameters = $prop_or_schema->fetch_arguments($this);
199
        if (count($this->current_parameters) > 0) {
200
            // TODO: add line info here.
201
            throw new \ParserException("Too much parameters.");
202
        }
203
        return $parameters;
204
    }
205
206
    protected function compile_qualifier(AST\Qualifier $node) {
207
        $which = $node->which();
208
        if ($which === AST\Qualifier::MUST) {
209
            return R\Rule::MODE_MUST;
210
        }
211
        if ($which == AST\Qualifier::CANNOT) {
212
            return R\Rule::MODE_CANNOT;
213
        }
214
        if ($which == AST\Qualifier::ONLY_X_CAN) {
215
            return R\Rule::MODE_ONLY_CAN;
216
        }
217
        throw new \LogicException("Unknown qualifier '$which'");
218
    }
219
220
    /**
221
     * Add a variable to the variables currently known.
222
     *
223
     * @param   string      $name
224
     * @param   V\Variable  $def
225
     * @return null
226
     */
227
    protected function add_variable($name, V\Variable $def) {
228
        assert('is_string($name)');
229
        if (array_key_exists($name, $this->variables)) {
230
            throw new ParserException("Variable '$name' already defined.");
231
        }
232
        assert('$def instanceof Lechimp\\Dicto\\Variables\\Variable');
233
        $this->variables[$name] = $def->withName($name);
234
    }
235
236
    /**
237
     * Get a predefined variable.
238
     *
239
     * @param   string  $name
240
     * @return  V\Variable
241
     */
242
    protected function get_variable($name) {
243
        if (!array_key_exists($name, $this->variables)) {
244
            throw new ParserException("Unknown variable '$name'.");
245
        }
246
        return $this->variables[$name];
247
    }
248
249
    /**
250
     * Get a property. Yes. Really.
251
     *
252
     * @param   string  $name
253
     * @return  V\Property
254
     */
255
    protected function get_property($name) {
256
        if (!array_key_exists($name, $this->properties)) {
257
            throw new ParserException("Unknown property '$name'.");
258
        }
259
        return $this->properties[$name];
260
    }
261
262
    /**
263
     * Get a schema. By name.
264
     *
265
     * @param   string  $name
266
     * @return  V\Schema
267
     */
268
    protected function get_schema($name) {
269
        if (!array_key_exists($name, $this->schemas)) {
270
            throw new ParserException("Unknown schema '$name'.");
271
        }
272
        return $this->schemas[$name];
273
    }
274
275
    /**
276
     * Add all predefined variables to the current set of variables.
277
     *
278
     * @return null
279
     */
280
    protected function add_predefined_variables() {
281
        foreach ($this->predefined_variables as $predefined_var) {
282
            $this->add_variable($predefined_var->name(), $predefined_var);
283
        }
284
    }
285
286
    /**
287
     * Purge all predefined variables from the current set of variables.
288
     *
289
     * @return null
290
     */
291
    protected function purge_predefined_variables() {
292
        foreach ($this->predefined_variables as $predefined_var) {
293
            unset($this->variables[$predefined_var->name()]);
294
        }
295
    }
296
297
    // IMPLEMENTATION OF ArgumentParser
298
299
    /**
300
     * @inheritdoc
301
     */
302
    public function fetch_string() {
303
        $node = $this->next_current_parameter(AST\StringValue::class);
304
        return "".$node;
305
    }
306
307
    /**
308
     * @inheritdoc
309
     */
310
    public function fetch_variable() {
311
        $node = $this->next_current_parameter(AST\Definition::class);
312
        return $this->compile_definition($node);
313
    }
314
315
    protected function next_current_parameter($class) {
316
        if (count($this->current_parameters) == 0) {
317
            // TODO: add location info
318
            throw new \ParserException("Tried to fetch a parameter, but none was left.");
319
        }
320
        $arg = array_shift($this->current_parameters);
321
        if (!($arg instanceof $class)) {
322
            // TODO: add location info
323
            throw new \ParserException("Tried to fetch a '%class' parameter but next is '".get_class($arg)."'");
324
        }
325
        return $arg;
326
    }
327
}
328