@@ -340,46 +340,57 @@ struct Threw <: ExecutionResult
340340 source:: LineNumberNode
341341end
342342
343- function eval_test (evaluated:: Expr , quoted:: Expr , source:: LineNumberNode , negate:: Bool = false )
344- evaled_args = evaluated. args
343+ function eval_test_comparison (comparison:: Expr , quoted:: Expr , source:: LineNumberNode , negate:: Bool = false )
344+ comparison. head === :comparison || throw (ArgumentError (" $comparison is not a comparison expression" ))
345+ comparison_args = comparison. args
345346 quoted_args = quoted. args
346- n = length (evaled_args )
347+ n = length (comparison_args )
347348 kw_suffix = " "
348- if evaluated. head === :comparison
349- args = evaled_args
350- res = true
351- i = 1
352- while i < n
353- a, op, b = args[i], args[i+ 1 ], args[i+ 2 ]
354- if res
355- res = op (a, b)
356- end
357- quoted_args[i] = a
358- quoted_args[i+ 2 ] = b
359- i += 2
360- end
361349
362- elseif evaluated. head === :call
363- op = evaled_args[1 ]
364- kwargs = (evaled_args[2 ]:: Expr ). args # Keyword arguments from `Expr(:parameters, ...)`
365- args = evaled_args[3 : n]
366-
367- res = op (args... ; kwargs... )
368-
369- # Create "Evaluated" expression which looks like the original call but has all of
370- # the arguments evaluated
371- func_sym = quoted_args[1 ]:: Union{Symbol,Expr}
372- if isempty (kwargs)
373- quoted = Expr (:call , func_sym, args... )
374- elseif func_sym === :≈ && ! res
375- quoted = Expr (:call , func_sym, args... )
376- kw_suffix = " ($(join ([" $k =$v " for (k, v) in kwargs], " , " )) )"
377- else
378- kwargs_expr = Expr (:parameters , [Expr (:kw , k, v) for (k, v) in kwargs]. .. )
379- quoted = Expr (:call , func_sym, kwargs_expr, args... )
350+ res = true
351+ i = 1
352+ while i < n
353+ a, op, b = comparison_args[i], comparison_args[i+ 1 ], comparison_args[i+ 2 ]
354+ if res
355+ res = op (a, b)
380356 end
357+ quoted_args[i] = a
358+ quoted_args[i+ 2 ] = b
359+ i += 2
360+ end
361+
362+ if negate
363+ res = ! res
364+ quoted = Expr (:call , :! , quoted)
365+ end
366+
367+ Returned (res,
368+ # stringify arguments in case of failure, for easy remote printing
369+ res === true ? quoted : sprint (print, quoted, context= (:limit => true )) * kw_suffix,
370+ source)
371+ end
372+
373+ function eval_test_function (func, args, kwargs, quoted_func:: Union{Expr,Symbol} , source:: LineNumberNode , negate:: Bool = false )
374+ res = func (args... ; kwargs... )
375+
376+ # Create "Evaluated" expression which looks like the original call but has all of
377+ # the arguments evaluated
378+ kw_suffix = " "
379+ if quoted_func === :≈ && ! res
380+ kw_suffix = " ($(join ([" $k =$v " for (k, v) in kwargs], " , " )) )"
381+ quoted_args = args
382+ elseif isempty (kwargs)
383+ quoted_args = args
381384 else
382- throw (ArgumentError (" Unhandled expression type: $(evaluated. head) " ))
385+ kwargs_expr = Expr (:parameters , [Expr (:kw , k, v) for (k, v) in kwargs]. .. )
386+ quoted_args = [kwargs_expr, args... ]
387+ end
388+
389+ # Properly render broadcast function call syntax, e.g. `(==).(1, 2)` or `Base.:(==).(1, 2)`.
390+ quoted = if isa (quoted_func, Expr) && quoted_func. head === :. && length (quoted_func. args) == 1
391+ Expr (:., quoted_func. args[1 ], Expr (:tuple , quoted_args... ))
392+ else
393+ Expr (:call , quoted_func, quoted_args... )
383394 end
384395
385396 if negate
@@ -576,14 +587,90 @@ macro test_skip(ex, kws...)
576587 return :(record (get_testset (), $ testres))
577588end
578589
579- function _can_escape_call (@nospecialize ex)
580- ex. head === :call || return false
590+ function _should_escape_call (@nospecialize ex)
591+ isa (ex, Expr) || return false
592+
593+ args = if ex. head === :call
594+ ex. args[2 : end ]
595+ elseif ex. head === :. && length (ex. args) == 2 && isa (ex. args[2 ], Expr) && ex. args[2 ]. head === :tuple
596+ # Support for broadcasted function calls (e.g. `(==).(1, 2)`)
597+ ex. args[2 ]. args
598+ else
599+ # Expression is not a function call
600+ return false
601+ end
602+
603+ # Avoid further processing on calls without any arguments
604+ return length (args) > 0
605+ end
606+
607+ # Escapes all of the positional arguments and keywords of a function such that we can call
608+ # the function at runtime.
609+ function _escape_call (@nospecialize ex)
610+ if isa (ex, Expr) && ex. head === :call
611+ # Update broadcast comparison calls to the function call syntax
612+ # (e.g. `1 .== 1` becomes `(==).(1, 1)`)
613+ func_str = string (ex. args[1 ])
614+ escaped_func = if first (func_str) == ' .'
615+ esc (Expr (:., Symbol (func_str[2 : end ])))
616+ else
617+ esc (ex. args[1 ])
618+ end
619+ quoted_func = QuoteNode (ex. args[1 ])
620+ args = ex. args[2 : end ]
621+ elseif isa (ex, Expr) && ex. head === :. && length (ex. args) == 2 && isa (ex. args[2 ], Expr) && ex. args[2 ]. head === :tuple
622+ # Support for broadcasted function calls (e.g. `(==).(1, 2)`)
623+ escaped_func = if isa (ex. args[1 ], Expr) && ex. args[1 ]. head == :.
624+ Expr (:call , Expr (:., :Broadcast , QuoteNode (:BroadcastFunction )), esc (ex. args[1 ]))
625+ else
626+ Expr (:., esc (ex. args[1 ]))
627+ end
628+ quoted_func = QuoteNode (Expr (:., ex. args[1 ]))
629+ args = ex. args[2 ]. args
630+ else
631+ throw (ArgumentError (" $ex is not a call expression" ))
632+ end
633+
634+ escaped_args = []
635+ escaped_kwargs = []
581636
582- # Broadcasted functions are not currently supported
583- first (string (ex. args[1 ])) != ' .' || return false
637+ # Positional arguments and keywords that occur before `;`. Note that the keywords are
638+ # being revised into a form we can splat.
639+ for a in args
640+ if isa (a, Expr) && a. head === :parameters
641+ continue
642+ elseif isa (a, Expr) && a. head === :kw
643+ # Keywords that occur before `;`. Note that the keywords are being revised into
644+ # a form we can splat.
645+ push! (escaped_kwargs, Expr (:call , :(=> ), QuoteNode (a. args[1 ]), esc (a. args[2 ])))
646+ elseif isa (a, Expr) && a. head === :...
647+ push! (escaped_args, Expr (:... , esc (a. args[1 ])))
648+ else
649+ push! (escaped_args, esc (a))
650+ end
651+ end
584652
585- # At least one positional argument or keyword
586- return length (ex. args) > 1
653+ # Keywords that occur after ';'
654+ if length (args) > 0 && isa (args[1 ], Expr) && args[1 ]. head === :parameters
655+ for kw in args[1 ]. args
656+ if isa (kw, Expr) && kw. head === :kw
657+ push! (escaped_kwargs, Expr (:call , :(=> ), QuoteNode (kw. args[1 ]), esc (kw. args[2 ])))
658+ elseif isa (kw, Expr) && kw. head === :...
659+ push! (escaped_kwargs, Expr (:... , esc (kw. args[1 ])))
660+ elseif isa (kw, Expr) && kw. head === :.
661+ push! (escaped_kwargs, Expr (:call , :(=> ), QuoteNode (kw. args[2 ]. value), esc (Expr (:., kw. args[1 ], QuoteNode (kw. args[2 ]. value)))))
662+ elseif isa (kw, Symbol)
663+ push! (escaped_kwargs, Expr (:call , :(=> ), QuoteNode (kw), esc (kw)))
664+ end
665+ end
666+ end
667+
668+ return (;
669+ func= escaped_func,
670+ args= escaped_args,
671+ kwargs= escaped_kwargs,
672+ quoted_func,
673+ )
587674end
588675
589676# An internal function, called by the code generated by the @test
@@ -613,60 +700,22 @@ function get_test_result(ex, source)
613700 ex = Expr (:comparison , ex. args[1 ], ex. head, ex. args[2 ])
614701 end
615702 if isa (ex, Expr) && ex. head === :comparison
616- # pass all terms of the comparison to `eval_comparison `, as an Expr
703+ # pass all terms of the comparison to `eval_test_comparison `, as a tuple
617704 escaped_terms = [esc (arg) for arg in ex. args]
618705 quoted_terms = [QuoteNode (arg) for arg in ex. args]
619- testret = :(eval_test (
706+ testret = :(eval_test_comparison (
620707 Expr (:comparison , $ (escaped_terms... )),
621708 Expr (:comparison , $ (quoted_terms... )),
622709 $ (QuoteNode (source)),
623710 $ negate,
624711 ))
625- elseif isa (ex, Expr) && _can_escape_call (ex)
626- escaped_func = esc (ex. args[1 ])
627- quoted_func = QuoteNode (ex. args[1 ])
628-
629- escaped_args = []
630- escaped_kwargs = []
631-
632- # Keywords that occur before `;`. Note that the keywords are being revised into
633- # a form we can splat.
634- for a in ex. args[2 : end ]
635- if isa (a, Expr) && a. head === :kw
636- push! (escaped_kwargs, Expr (:call , :(=> ), QuoteNode (a. args[1 ]), esc (a. args[2 ])))
637- end
638- end
639-
640- # Keywords that occur after ';'
641- parameters_expr = ex. args[2 ]
642- if isa (parameters_expr, Expr) && parameters_expr. head === :parameters
643- for a in parameters_expr. args
644- if isa (a, Expr) && a. head === :kw
645- push! (escaped_kwargs, Expr (:call , :(=> ), QuoteNode (a. args[1 ]), esc (a. args[2 ])))
646- elseif isa (a, Expr) && a. head === :...
647- push! (escaped_kwargs, Expr (:... , esc (a. args[1 ])))
648- elseif isa (a, Expr) && a. head === :.
649- push! (escaped_kwargs, Expr (:call , :(=> ), QuoteNode (a. args[2 ]. value), esc (Expr (:., a. args[1 ], QuoteNode (a. args[2 ]. value)))))
650- elseif isa (a, Symbol)
651- push! (escaped_kwargs, Expr (:call , :(=> ), QuoteNode (a), esc (a)))
652- end
653- end
654- end
655-
656- # Positional arguments
657- for a in ex. args[2 : end ]
658- isa (a, Expr) && a. head in (:kw , :parameters ) && continue
659-
660- if isa (a, Expr) && a. head === :...
661- push! (escaped_args, Expr (:... , esc (a. args[1 ])))
662- else
663- push! (escaped_args, esc (a))
664- end
665- end
666-
667- testret = :(eval_test (
668- Expr (:call , $ escaped_func, Expr (:parameters , $ (escaped_kwargs... )), $ (escaped_args... )),
669- Expr (:call , $ quoted_func),
712+ elseif _should_escape_call (ex)
713+ call = _escape_call (ex)
714+ testret = :(eval_test_function (
715+ $ (call. func),
716+ ($ (call. args... ),),
717+ ($ (call. kwargs... ),),
718+ $ (call. quoted_func),
670719 $ (QuoteNode (source)),
671720 $ negate,
672721 ))
0 commit comments