]>
git.ipfire.org Git - thirdparty/dhcp.git/blob - keama/reduce.c
9c79645862392fcf6b92b51d24a6775a2e8996a5
2 * Copyright (c) 2017 by Internet Systems Consortium, Inc. ("ISC")
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
14 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 * Internet Systems Consortium, Inc.
18 * Newmarket, NH 03857 USA
20 * https://www.isc.org/
26 #include <sys/errno.h>
27 #include <sys/types.h>
28 #include <arpa/inet.h>
36 static struct element
*reduce_equal_expression(struct element
*left
,
37 struct element
*right
);
38 static void debug(const char* fmt
, ...);
41 * boolean_expression :== CHECK STRING |
42 * NOT boolean-expression |
43 * data-expression EQUAL data-expression |
44 * data-expression BANG EQUAL data-expression |
45 * data-expression REGEX_MATCH data-expression |
46 * boolean-expression AND boolean-expression |
47 * boolean-expression OR boolean-expression
52 reduce_boolean_expression(struct element
*expr
)
54 /* trivial case: already done */
55 if (expr
->type
== ELEMENT_BOOLEAN
)
59 * From is_boolean_expression
62 if (expr
->type
!= ELEMENT_MAP
)
66 if (mapContains(expr
, "check"))
68 * syntax := { "check": <collection_name> }
69 * semantic: check_collection
70 * on server try to match classes of the collection
76 if (mapContains(expr
, "exists")) {
78 * syntax := { "exists":
79 * { "universe": <option_space_old>,
80 * "name": <option_name> }
82 * semantic: check universe/code from incoming packet
85 struct element
*universe
;
87 struct option
*option
;
90 arg
= mapGet(expr
, "exists");
91 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
92 debug("can't get exists argument");
95 universe
= mapGet(arg
, "universe");
96 if ((universe
== NULL
) || (universe
->type
!= ELEMENT_STRING
)) {
97 debug("can't get exists option universe");
100 name
= mapGet(arg
, "name");
101 if ((name
== NULL
) || (name
->type
!= ELEMENT_STRING
)) {
102 debug("can't get exists option name");
105 option
= option_lookup_name(stringValue(universe
)->content
,
106 stringValue(name
)->content
);
107 if ((option
== NULL
) || (option
->code
== 0))
109 if (((local_family
== AF_INET
) &&
110 (strcmp(option
->space
->name
, "dhcp4") != 0)) ||
111 ((local_family
== AF_INET6
) &&
112 (strcmp(option
->space
->name
, "dhcp6") != 0)))
114 snprintf(result
, sizeof(result
),
115 "option[%u].exists", option
->code
);
116 return createString(makeString(-1, result
));
119 /* variable-exists */
120 if (mapContains(expr
, "variable-exists"))
122 * syntax := { "variable-exists": <variable_name> }
123 * semantics: find_binding(scope, name)
128 if (mapContains(expr
, "equal")) {
130 * syntax := { "equal":
131 * { "left": <expression>,
132 * "right": <expression> }
134 * semantics: evaluate branches and return true
135 * if same type and same value
138 struct element
*left
;
139 struct element
*right
;
141 arg
= mapGet(expr
, "equal");
142 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
143 debug("can't get equal argument");
146 left
= mapGet(arg
, "left");
148 debug("can't get equal left branch");
151 right
= mapGet(arg
, "right");
153 debug("can't get equal right branch");
156 return reduce_equal_expression(left
, right
);
160 if (mapContains(expr
, "not-equal")) {
162 * syntax := { "not-equal":
163 * { "left": <expression>,
164 * "right": <expression> }
166 * semantics: evaluate branches and return true
167 * if different type or different value
170 struct element
*left
;
171 struct element
*right
;
172 struct element
*equal
;
173 struct string
*result
;
175 arg
= mapGet(expr
, "not-equal");
176 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
177 debug("can't get not-equal argument");
180 left
= mapGet(arg
, "left");
182 debug("can't get not-equal left branch");
185 right
= mapGet(arg
, "right");
187 debug("can't get not-equal right branch");
190 equal
= reduce_equal_expression(left
, right
);
191 if ((equal
== NULL
) || (equal
->type
!= ELEMENT_STRING
))
193 result
= makeString(-1, "not (");
194 concatString(result
, stringValue(equal
));
195 appendString(result
, ")");
196 return createString(result
);
200 if (mapContains(expr
, "regex-match"))
202 * syntax := { "regex-match":
203 * { "left": <data_expression>,
204 * "right": <data_expression> }
206 * semantics: evaluate branches, compile right as a
207 * regex and apply it to left
212 if (mapContains(expr
, "iregex-match"))
214 * syntax := { "regex-match":
215 * { "left": <data_expression>,
216 * "right": <data_expression> }
218 * semantics: evaluate branches, compile right as a
219 * case insensistive regex and apply it to left
224 if (mapContains(expr
, "and")) {
227 * { "left": <boolean_expression>,
228 * "right": <boolean_expression> }
230 * semantics: evaluate branches, return true
234 struct element
*left
;
235 struct element
*right
;
236 struct string
*result
;
238 arg
= mapGet(expr
, "and");
239 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
240 debug("can't get and argument");
243 left
= mapGet(arg
, "left");
245 debug("can't get and left branch");
248 right
= mapGet(arg
, "right");
250 debug("can't get and right branch");
253 left
= reduce_boolean_expression(left
);
254 if ((left
== NULL
) || (left
->type
!= ELEMENT_STRING
))
256 right
= reduce_boolean_expression(right
);
257 if ((right
== NULL
) || (right
->type
!= ELEMENT_STRING
))
259 result
= makeString(-1, "(");
260 concatString(result
, stringValue(left
));
261 appendString(result
, ") and (");
262 concatString(result
, stringValue(right
));
263 appendString(result
, ")");
264 return createString(result
);
268 if (mapContains(expr
, "or")) {
271 * { "left": <boolean_expression>,
272 * "right": <boolean_expression> }
274 * semantics: evaluate branches, return true
278 struct element
*left
;
279 struct element
*right
;
280 struct string
*result
;
282 arg
= mapGet(expr
, "or");
283 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
284 debug("can't get or argument");
287 left
= mapGet(arg
, "left");
289 debug("can't get or left branch");
292 right
= mapGet(arg
, "right");
294 debug("can't get or right branch");
297 left
= reduce_boolean_expression(left
);
298 if ((left
== NULL
) || (left
->type
!= ELEMENT_STRING
))
300 right
= reduce_boolean_expression(right
);
301 if ((right
== NULL
) || (right
->type
!= ELEMENT_STRING
))
303 result
= makeString(-1, "(");
304 concatString(result
, stringValue(left
));
305 appendString(result
, ") or (");
306 concatString(result
, stringValue(right
));
307 appendString(result
, ")");
308 return createString(result
);
312 if (mapContains(expr
, "not")) {
314 * syntax := { "not": <boolean_expression> }
315 * semantic: evaluate its branch and return its negation
318 struct string
*result
;
320 arg
= mapGet(expr
, "not");
322 debug("can't get not argument");
325 arg
= reduce_boolean_expression(arg
);
326 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_STRING
))
328 result
= makeString(-1, "not (");
329 concatString(result
, stringValue(arg
));
330 appendString(result
, ")");
331 return createString(result
);
335 if (mapContains(expr
, "known"))
337 * syntax := { "known": null }
338 * semantics: client is known, i.e., has a matching
339 * host declaration (aka reservation in Kea)
344 if (mapContains(expr
, "static"))
346 * syntax := { "static": null }
347 * semantics: lease is static (doesn't exist in Kea)
355 * data_expression :== SUBSTRING LPAREN data-expression COMMA
356 * numeric-expression COMMA
357 * numeric-expression RPAREN |
358 * CONCAT LPAREN data-expression COMMA
359 * data-expression RPAREN
360 * SUFFIX LPAREN data_expression COMMA
361 * numeric-expression RPAREN |
362 * LCASE LPAREN data_expression RPAREN |
363 * UCASE LPAREN data_expression RPAREN |
364 * OPTION option_name |
366 * PACKET LPAREN numeric-expression COMMA
367 * numeric-expression RPAREN |
368 * V6RELAY LPAREN numeric-expression COMMA
369 * data-expression RPAREN |
371 * colon_separated_hex_list
375 reduce_data_expression(struct element
*expr
)
377 /* trivial case: already done */
378 if (expr
->type
== ELEMENT_STRING
)
382 * From is_data_expression
385 if (expr
->type
!= ELEMENT_MAP
)
389 if (mapContains(expr
, "substring")) {
391 * syntax := { "substring":
392 * { "expression": <data_expression>,
393 * "offset": <numeric_expression>,
394 * "length": <numeric_expression> }
396 * semantic: evaluate arguments, if the string is
397 * shorter than offset return "" else return substring
400 struct element
*string
;
401 struct element
*offset
;
402 struct element
*length
;
403 struct string
*result
;
408 arg
= mapGet(expr
, "substring");
409 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
410 debug("can't get substring argument");
413 string
= mapGet(arg
, "expression");
414 if (string
== NULL
) {
415 debug("can't get substring expression");
418 offset
= mapGet(arg
, "offset");
419 if (offset
== NULL
) {
420 debug("can't get substring offset");
423 length
= mapGet(arg
, "length");
424 if (length
== NULL
) {
425 debug("can't get substring length");
428 /* can't be a literal as it was evaluated before */
429 string
= reduce_data_expression(string
);
430 if ((string
== NULL
) || (string
->type
!= ELEMENT_STRING
))
432 offset
= reduce_numeric_expression(offset
);
433 if ((offset
== NULL
) || (offset
->type
!= ELEMENT_INTEGER
))
435 off
= intValue(offset
);
437 debug("substring with a negative offset (%lld)",
441 length
= reduce_numeric_expression(length
);
442 if ((length
== NULL
) || (length
->type
!= ELEMENT_INTEGER
))
444 len
= intValue(length
);
446 debug("substring with a negative length (%lld)",
450 result
= makeString(-1, "substring(");
451 concatString(result
, stringValue(string
));
452 snprintf(buf
, sizeof(buf
),
453 ",%u,%u)", (unsigned)off
, (unsigned)len
);
454 appendString(result
, buf
);
455 return createString(result
);
459 if (mapContains(expr
, "suffix")) {
461 * syntax := { "suffix":
462 * { "expression": <data_expression>,
463 * "length": <numeric_expression> }
465 * semantic: evaluate arguments, if the string is
466 * shorter than length return it else return suffix
469 struct element
*string
;
470 struct element
*length
;
471 struct string
*result
;
475 arg
= mapGet(expr
, "suffix");
476 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
477 debug("can't get suffix argument");
480 string
= mapGet(arg
, "expression");
481 if (string
== NULL
) {
482 debug("can't get suffix expression");
485 length
= mapGet(arg
, "length");
486 if (length
== NULL
) {
487 debug("can't get suffix length");
490 /* can't be a literal as it was evaluated before */
491 string
= reduce_data_expression(string
);
492 if ((string
== NULL
) || (string
->type
!= ELEMENT_STRING
))
494 length
= reduce_numeric_expression(length
);
495 if ((length
== NULL
) || (length
->type
!= ELEMENT_INTEGER
))
497 len
= intValue(length
);
499 debug("suffix with a negative length (%lld)",
503 result
= makeString(-1, "substring(");
504 concatString(result
, stringValue(string
));
505 snprintf(buf
, sizeof(buf
), ",-%u,all)", (unsigned)len
);
506 appendString(result
, buf
);
507 return createString(result
);
511 if (mapContains(expr
, "lowercase"))
513 * syntax := { "lowercase": <data_expression> }
514 * semantic: evaluate its argument and apply tolower to
520 if (mapContains(expr
, "uppercase"))
522 * syntax := { "uppercase": <data_expression> }
523 * semantic: evaluate its argument and apply toupper to
529 if (mapContains(expr
, "option")) {
531 * syntax := { "option":
532 * { "universe": <option_space_old>,
533 * "name": <option_name> }
535 * semantic: get universe/code option from incoming packet
538 struct element
*universe
;
539 struct element
*name
;
540 struct option
*option
;
543 arg
= mapGet(expr
, "option");
544 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
545 debug("can't get option argument");
548 universe
= mapGet(arg
, "universe");
549 if ((universe
== NULL
) || (universe
->type
!= ELEMENT_STRING
)) {
550 debug("can't get option universe");
553 name
= mapGet(arg
, "name");
554 if ((name
== NULL
) || (name
->type
!= ELEMENT_STRING
)) {
555 debug("can't get option name");
558 option
= option_lookup_name(stringValue(universe
)->content
,
559 stringValue(name
)->content
);
560 if ((option
== NULL
) || (option
->code
== 0))
562 if (((local_family
== AF_INET
) &&
563 (strcmp(option
->space
->name
, "dhcp4") != 0)) ||
564 ((local_family
== AF_INET6
) &&
565 (strcmp(option
->space
->name
, "dhcp6") != 0)))
567 snprintf(result
, sizeof(result
),
568 "option[%u].hex", option
->code
);
569 return createString(makeString(-1, result
));
573 if (mapContains(expr
, "hardware")) {
575 * syntax := { "hardware": null }
576 * semantic: get mac type and address from incoming packet
578 struct string
*result
;
580 if (local_family
!= AF_INET
) {
581 debug("get hardware for DHCPv6");
584 result
= makeString(-1,
585 "concat(substring(pkt4.htype,-1,all),pkt4.mac)");
586 return createString(result
);
590 if (mapContains(expr
, "hw-type")) {
593 * syntax := { "hw-type": null }
594 * semantic: get mac type from incoming packet
596 struct string
*result
;
598 if (local_family
!= AF_INET
) {
599 debug("get hw-type for DHCPv6");
602 result
= makeString(-1, "substring(pkt4.htype,-1,all)");
603 return createString(result
);
607 if (mapContains(expr
, "hw-address")) {
610 * syntax := { "hw-address": null }
611 * semantic: get mac address from incoming packet
613 struct string
*result
;
615 if (local_family
!= AF_INET
) {
616 debug("get hw-address for DHCPv6");
619 result
= makeString(-1, "pkt4.mac");
620 return createString(result
);
624 if (mapContains(expr
, "const-data")) {
626 * syntax := { "const-data": <string> }
627 * semantic: embedded string value
631 arg
= mapGet(expr
, "const-data");
632 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_STRING
)) {
633 debug("can't get const-data argument");
636 return createString(stringValue(arg
));
640 if (mapContains(expr
, "packet"))
642 * syntax := { "packet":
643 * { "offset": <numeric_expression>,
644 * "length": <numeric_expression> }
646 * semantic: return the selected substring of the incoming
652 if (mapContains(expr
, "concat")) {
654 * syntax := { "concat":
655 * { "left": <data_expression>,
656 * "right": <data_expression> }
658 * semantic: evaluate arguments and return the concatenation
661 struct element
*left
;
662 struct element
*right
;
663 struct string
*result
;
665 arg
= mapGet(expr
, "concat");
666 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
667 debug("can't get concat argument");
670 left
= mapGet(arg
, "left");
672 debug("can't get concat left branch");
675 right
= mapGet(arg
, "right");
677 debug("can't get concat right branch");
680 /* left is a literal case */
681 if (left
->type
== ELEMENT_STRING
) {
682 /* can't be a literal as it was evaluated before */
683 right
= reduce_data_expression(right
);
684 if ((right
== NULL
) || (right
->type
!= ELEMENT_STRING
))
686 result
= makeString(-1, "concat(");
687 concatString(result
, quote(stringValue(left
)));
688 appendString(result
, ", ");
689 concatString(result
, stringValue(right
));
690 appendString(result
, ")");
691 return createString(result
);
693 left
= reduce_data_expression(left
);
694 if ((left
== NULL
) || (left
->type
!= ELEMENT_STRING
))
696 /* right is a literal case */
697 if (right
->type
== ELEMENT_STRING
) {
698 /* literal left was handled before */
699 result
= makeString(-1, "concat(");
700 concatString(result
, stringValue(left
));
701 appendString(result
, ", ");
702 concatString(result
, quote(stringValue(right
)));
703 appendString(result
, ")");
704 return createString(result
);
706 right
= reduce_data_expression(right
);
707 if ((right
== NULL
) || (right
->type
!= ELEMENT_STRING
))
709 result
= makeString(-1, "concat(");
710 concatString(result
, stringValue(left
));
711 appendString(result
, ", ");
712 concatString(result
, stringValue(right
));
713 appendString(result
, ")");
714 return createString(result
);
718 if (mapContains(expr
, "encapsulate"))
720 * syntax := { "encapsulate": <encapsulated_space> }
721 * semantic: encapsulate options of the given space
726 if (mapContains(expr
, "encode-int8"))
728 * syntax := { "encode-int8": <numeric_expression> }
729 * semantic: return a string buffer with the evaluated
735 if (mapContains(expr
, "encode-int16"))
737 * syntax := { "encode-int16": <numeric_expression> }
738 * semantic: return a string buffer with the evaluated
744 if (mapContains(expr
, "encode-int32"))
746 * syntax := { "encode-int32": <numeric_expression> }
747 * semantic: return a string buffer with the evaluated
753 if (mapContains(expr
, "gethostbyname"))
755 * syntax := { "gethostbyname": <string> }
756 * semantic: call gethostbyname and return
757 * a binary buffer with addresses
761 /* binary-to-ascii */
762 if (mapContains(expr
, "binary-to-ascii"))
764 * syntax := { "binary-to-ascii":
765 * { "base": <numeric_expression 2..16>,
766 * "width": <numeric_expression 8, 16 or 32>,
767 * "separator": <data_expression>,
768 * "buffer": <data_expression> }
770 * semantic: split the input buffer into int8/16/32 numbers,
771 * output them separated by the given string
776 if (mapContains(expr
, "filename"))
778 * syntax := { "filename": null }
779 * semantic: get filename field from incoming DHCPv4 packet
784 if (mapContains(expr
, "server-name"))
786 * syntax := { "server-name": null }
787 * semantic: get server-name field from incoming DHCPv4 packet
792 if (mapContains(expr
, "reverse"))
794 * syntax := { "reverse":
795 * { "width": <numeric_expression>,
796 * "buffer": <data_expression> }
798 * semantic: reverse the input buffer by width chunks of bytes
802 /* pick-first-value */
803 if (mapContains(expr
, "pick-first-value"))
805 * syntax := { "pick-first-value":
806 * [ <data_expression>, ... ]
808 * semantic: evaluates expressions and return the first
809 * not null, return null if all are null
814 if (mapContains(expr
, "host-decl-name"))
816 * syntax := { "host-decl-name": null }
817 * semantic: return the name of the matching host
818 * declaration (aka revervation in kea) or null
823 if (mapContains(expr
, "leased-address"))
825 * syntax := { "leased-address": null }
826 * semantic: return the address of the assigned lease or
832 if (mapContains(expr
, "config-option"))
834 * syntax := { "config-option":
835 * { "universe": <option_space_old>,
836 * "name": <option_name> }
838 * semantic: get universe/code option to send
843 if (mapContains(expr
, "null")) {
845 * syntax := { "null": null }
846 * semantic: return null
848 debug("unexpected null: this expression was not evaluated");
853 if (mapContains(expr
, "gethostname")) {
855 * syntax := { "gethostname": null }
856 * semantic: return gethostname
858 debug("unexpected gethostname: this expression was not "
864 if (mapContains(expr
, "v6relay")) {
866 * syntax := { "v6relay":
867 * { "relay": <numeric_expression>,
868 * "relay-option" <data_expression> }
870 * semantic: relay is a counter from client, 0 is no-op,
871 * 1 is the relay closest to the client, etc, option
872 * is a dhcp6 option ans is return when found
875 struct element
*relay
;
876 struct element
*universe
;
877 struct element
*name
;
878 struct option
*option
;
882 if (local_family
!= AF_INET6
) {
883 debug("get v6relay for DHCPv4");
886 arg
= mapGet(expr
, "v6relay");
887 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
888 debug("can't get v6relay argument");
891 relay
= mapGet(arg
, "relay");
893 debug("can't get v6relay relay");
896 relay
= reduce_numeric_expression(relay
);
897 if ((relay
== NULL
) || (relay
->type
!= ELEMENT_INTEGER
))
901 debug("v6relay called with illegal relay (%lld)",
905 arg
= mapGet(arg
, "relay-option");
906 if ((arg
== NULL
) || (arg
->type
!= ELEMENT_MAP
)) {
907 debug("can't get v6relay relay-option");
910 universe
= mapGet(arg
, "universe");
911 if ((universe
== NULL
) || (universe
->type
!= ELEMENT_STRING
)) {
912 debug("can't get v6relay option universe");
915 name
= mapGet(arg
, "name");
916 if ((name
== NULL
) || (name
->type
!= ELEMENT_STRING
)) {
917 debug("can't get v6relay option name");
920 option
= option_lookup_name(stringValue(universe
)->content
,
921 stringValue(name
)->content
);
922 if ((option
== NULL
) || (option
->code
== 0) ||
923 (strcmp(option
->space
->name
, "dhcp6") != 0))
926 snprintf(result
, sizeof(result
),
927 "option[%u].hex", option
->code
);
929 /* r > MAX_V6RELAY_HOPS means the relay closest
931 if (r
> MAX_V6RELAY_HOPS
)
933 /* Kea counts from the server, use negative nesting
934 levels to count from the client */
935 snprintf(result
, sizeof(result
),
936 "relay6[%d].option[%u].hex",
937 (int)-r
, option
->code
);
939 return createString(makeString(-1, result
));
946 reduce_numeric_expression(struct element
*expr
)
948 /* trivial case: already done */
949 if (expr
->type
== ELEMENT_INTEGER
)
952 if (expr
->type
!= ELEMENT_MAP
)
955 /* Kea has no numeric operators... */
959 static struct element
*
960 reduce_equal_expression(struct element
*left
, struct element
*right
)
962 struct string
*result
;
965 * numeric case was handled by evaluation
968 if (!is_data_expression(left
) || !is_data_expression(right
))
971 /* left is a literal case */
972 if (left
->type
== ELEMENT_STRING
) {
973 /* can't be a literal as it was evaluated before */
974 right
= reduce_data_expression(right
);
975 if ((right
== NULL
) || (right
->type
!= ELEMENT_STRING
))
977 result
= allocString();
978 concatString(result
, quote(stringValue(left
)));
979 appendString(result
, " == ");
980 concatString(result
, stringValue(right
));
981 return createString(result
);
983 left
= reduce_data_expression(left
);
984 if ((left
== NULL
) || (left
->type
!= ELEMENT_STRING
))
987 /* right is a literal case */
988 if (right
->type
== ELEMENT_STRING
) {
989 /* literal left was handled before */
990 result
= allocString();
991 concatString(result
, stringValue(left
));
992 appendString(result
, " == ");
993 concatString(result
, quote(stringValue(right
)));
994 return createString(result
);
996 right
= reduce_data_expression(right
);
997 if ((right
== NULL
) || (right
->type
!= ELEMENT_STRING
))
1000 result
= allocString();
1001 concatString(result
, stringValue(left
));
1002 appendString(result
, " == ");
1003 concatString(result
, stringValue(right
));
1004 return createString(result
);
1008 debug(const char* fmt
, ...)
1012 va_start(list
, fmt
);
1013 vfprintf(stderr
, fmt
, list
);
1014 fprintf(stderr
, "\n");