# Commands covered:  tailcall
#
# This file contains a collection of tests for experimental commands that are
# found in ::tcl::unsupported. The tests will migrate to normal test files
# if/when the commands find their way into the core.
#
# Copyright (c) 2008 by Miguel Sofer.
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.

if {[lsearch [namespace children] ::tcltest] == -1} {
    package require tcltest
    namespace import -force ::tcltest::*
}

::tcltest::loadTestedCommands
catch [list package require -exact Tcltest [info patchlevel]]

testConstraint testnrelevels [llength [info commands testnrelevels]]

#
# The tests that risked blowing the C stack on failure have been removed: we
# can now actually measure using testnrelevels.
#

if {[testConstraint testnrelevels]} {
    namespace eval testnre {
	#
	# [testnrelevels] returns a 6-list with: C-stack depth, iPtr->numlevels,
	# cmdFrame level, callFrame level, tosPtr and callback depth
	#
	variable last [testnrelevels]
	proc depthDiff {} {
	    variable last
	    set depth [testnrelevels]
	    set res {}
	    foreach t $depth l $last {
		lappend res [expr {$t-$l}]
	    }
	    set last $depth
	    return $res
	}
	namespace export *
    }
    namespace import testnre::*
}

proc errorcode options {
    dict get [dict merge {-errorcode NONE} $options] -errorcode
}

test tailcall-0.1 {tailcall is constant space} -constraints testnrelevels -setup {
    proc a i {
	#
	# NOTE: there may be a diff in callback depth with the first call
	# ($i==0) due to the fact that the first is from an eval. Successive
	# calls should add nothing to any stack depths.
	#
	if {$i == 1} {
	    depthDiff
	}
	if {[incr i] > 10} {
	    return [depthDiff]
	}
	tailcall a $i
    }
} -body {
    a 0
} -cleanup {
    rename a {}
} -result {0 0 0 0 0 0}

test tailcall-0.2 {tailcall is constant space} -constraints testnrelevels -setup {
    set a { i {
	if {$i == 1} {
	    depthDiff
	}
	if {[incr i] > 10} {
	    return [depthDiff]
	}
	upvar 1 a a
	tailcall apply $a $i
    }}
} -body {
    apply $a 0
} -cleanup {
    unset a
} -result {0 0 0 0 0 0}

test tailcall-0.3 {tailcall is constant space} -constraints testnrelevels -setup {
    proc a i {
	if {$i == 1} {
	    depthDiff
	}
	if {[incr i] > 10} {
	    return [depthDiff]
	}
	tailcall b $i
    }
    interp alias {} b {} a
} -body {
    b 0
} -cleanup {
    rename a {}
    rename b {}
} -result {0 0 0 0 0 0}

test tailcall-0.4 {tailcall is constant space} -constraints testnrelevels -setup {
    namespace eval ::ns {
	namespace export *
    }
    proc ::ns::a i {
	if {$i == 1} {
	    depthDiff
	}
	if {[incr i] > 10} {
	    return [depthDiff]
	}
	set b [uplevel 1 [list namespace which b]]
	tailcall $b $i
    }
    namespace import ::ns::a
    rename a b
} -body {
    b 0
} -cleanup {
    rename b {}
    namespace delete ::ns
} -result {0 0 0 0 0 0}

test tailcall-0.5 {tailcall is constant space} -constraints testnrelevels -setup {
    proc b i {
	if {$i == 1} {
	    depthDiff
	}
	if {[incr i] > 10} {
	    return [depthDiff]
	}
	tailcall a b $i
    }
    namespace ensemble create -command a -map {b b}
} -body {
    a b 0
} -cleanup {
    rename a {}
    rename b {}
} -result {0 0 0 0 0 0}

test tailcall-0.5.1 {tailcall is constant space} -constraints testnrelevels -setup {
    #
    # This test is related to [bug d87cb182053fd79b3]: the fix to that bug was
    # to remove a call to TclSkipTailcall, which caused a violation of the
    # constant-space property of tailcall in that particular
    # configuration. This test was added to detect that, and insure that the
    # problem is fixed.
    #

    proc b i {
	if {$i == 1} {
	    depthDiff
	}
	if {[incr i] > 10} {
	    return [depthDiff]
	}
	tailcall dict b $i
    }
    set map0 [namespace ensemble configure dict -map]
    set map $map0
    dict set map b b
    namespace ensemble configure dict -map $map
} -body {
    dict b 0
} -cleanup {
    rename b {}
    namespace ensemble configure dict -map $map0
    unset map map0
} -result {0 0 0 0 0 0}

test tailcall-0.6 {tailcall is constant space} -constraints {testnrelevels knownBug} -setup {
    #
    # This test fails because ns-unknown is not NR-enabled
    #
    proc c i {
	if {$i == 1} {
	    depthDiff
	}
	if {[incr i] > 10} {
	    return [depthDiff]
	}
	tailcall a b $i
    }
    proc d {ens sub args} {
	return [list $ens c]
    }
    namespace ensemble create -command a -unknown d
} -body {
    a b 0
} -cleanup {
    rename a {}
    rename c {}
    rename d {}
} -result {0 0 0 0 0 0}

test tailcall-0.7 {tailcall is constant space} -constraints testnrelevels -setup {
    catch {rename foo {}}
    oo::class create foo {
	method b i {
	    if {$i == 1} {
		depthDiff
	    }
	    if {[incr i] > 10} {
		return [depthDiff]
	    }
	    tailcall [self] b $i
	}
    }
} -body {
    foo create a
    a b 0
} -cleanup {
    rename a {}
    rename foo {}
} -result {0 0 0 0 0 0}

test tailcall-1 {tailcall} -body {
    namespace eval a {
	variable x *::a
	proc xset {} {
	    set tmp {}
	    set ns {[namespace current]}
	    set level [info level]
	    for {set i 0} {$i <= [info level]} {incr i} {
		uplevel #$i "set x $i$ns"
		lappend tmp "$i [info level $i]"
	    }
	    lrange $tmp 1 end
	}
	proc foo {} {tailcall xset; set x noreach}
    }
    namespace eval b {
	variable x *::b
	proc xset args {error b::xset}
	proc moo {} {set x 0; variable y [::a::foo]; set x}
    }
    variable x *::
    proc xset args {error ::xset}
    list [::b::moo] | $x $a::x $b::x | $::b::y
} -cleanup {
    unset x
    rename xset {}
    namespace delete a b
} -result {1::b | 0:: *::a *::b | {{1 ::b::moo} {2 xset}}}


test tailcall-2 {tailcall in non-proc} -body {
    namespace eval a [list tailcall set x 1]
} -match glob -result *tailcall* -returnCodes error

test tailcall-3 {tailcall falls off tebc} -body {
    unset -nocomplain x
    proc foo {} {tailcall set x 1}
    list [catch foo msg] $msg [set x]
} -cleanup {
    rename foo {}
    unset x
} -result {0 1 1}

test tailcall-4 {tailcall falls off tebc} -body {
    set x 2
    proc foo {} {tailcall set x 1}
    foo
    set x
} -cleanup {
    rename foo {}
    unset x
} -result 1

test tailcall-5 {tailcall falls off tebc} -body {
    set x 2
    namespace eval bar {
	variable x 3
	proc foo {} {tailcall set x 1}
    }
    bar::foo
    list $x $bar::x
} -cleanup {
    unset x
    namespace delete bar
} -result {1 3}

test tailcall-6 {tailcall does remove callframes} -body {
    proc foo {} {info level}
    proc moo {} {tailcall foo}
    proc boo {} {expr {[moo] - [info level]}}
    boo
} -cleanup {
    rename foo {}
    rename moo {}
    rename boo {}
} -result 1

test tailcall-7 {tailcall does return} -setup {
    namespace eval ::foo {
	variable res {}
	proc a {} {
	    variable res
	    append res a
	    tailcall set x 1
	    append res a
	}
	proc b {} {
	    variable res
	    append res b
	    a
	    append res b
	}
	proc c {} {
	    variable res
	    append res c
	    b
	    append res c
	}
    }
} -body {
    namespace eval ::foo c
} -cleanup {
    namespace delete ::foo
} -result cbabc

test tailcall-8 {tailcall tailcall} -setup {
    namespace eval ::foo {
	variable res {}
	proc a {} {
	    variable res
	    append res a
	    tailcall tailcall set x 1
	    append res a
	}
	proc b {} {
	    variable res
	    append res b
	    a
	    append res b
	}
	proc c {} {
	    variable res
	    append res c
	    b
	    append res c
	}
    }
} -body {
    namespace eval ::foo c
} -cleanup {
    namespace delete ::foo
} -result cbac

test tailcall-9 {tailcall factorial} -setup {
    proc fact {n {b 1}} {
	if {$n == 1} {
	    return $b
	}
	tailcall fact [expr {$n-1}] [expr {$n*$b}]
    }
} -body {
    list [fact 1] [fact 5] [fact 10] [fact 15]
} -cleanup {
    rename fact {}
} -result {1 120 3628800 1307674368000}

test tailcall-10a {tailcall and eval} -setup {
    set ::x 0
    proc a {} {
	eval [list tailcall lappend ::x 2]
	set ::x 1
    }
} -body {
    list [a] $::x
} -cleanup {
    unset -nocomplain ::x
} -result {{0 2} {0 2}}

test tailcall-10b {tailcall and eval} -setup {
    set ::x 0
    proc a {} {
	eval {tailcall lappend ::x 2}
	set ::x 1
    }
} -body {
    list [a] $::x
} -cleanup {
    unset -nocomplain ::x
} -result {{0 2} {0 2}}

test tailcall-11a {tailcall and uplevel} -setup {
    proc a {} {
	uplevel 1 [list tailcall set ::x 2]
	set ::x 1
    }
} -body {
    list [a] $::x
} -cleanup {
    unset -nocomplain ::x
} -match glob -result *tailcall* -returnCodes error

test tailcall-11b {tailcall and uplevel} -setup {
    proc a {} {
	uplevel 1 {tailcall set ::x 2}
	set ::x 1
    }
} -body {
    list [a] $::x
} -cleanup {
    unset -nocomplain ::x
} -match glob -result *tailcall* -returnCodes error

test tailcall-11c {tailcall and uplevel} -setup {
    proc a {} {
	uplevel 1 {tailcall lappend ::x 2}
	set ::x 1
    }
    proc b {} {set ::x 0; a; lappend ::x 3}
} -body {
    list [b] $::x
} -cleanup {
    rename a {}
    rename b {}
    unset -nocomplain ::x
} -result {{0 3 2} {0 3 2}}

test tailcall-12.1 {[Bug 2649975]} -setup {
    proc dump {{text {}}} {
	set text [uplevel 1 [list subst $text]]
	set l [expr {[info level] -1}]
	if {$text eq {}} {
	    set text [info level $l]
	}
	puts "$l: $text"
    }
    # proc dump args {}
    proc bravo {} {
	upvar 1 v w
	dump {inside bravo, v -> $w}
	set v "procedure bravo"
	#uplevel 1 [list delta ::betty]
	uplevel 1 {delta ::betty}
	return $::resolution
    }
    proc delta name {
	upvar 1 v w
	dump {inside delta, v -> $w}
	set v "procedure delta"
	tailcall foxtrot
    }
    proc foxtrot {} {
	upvar 1 v w
	dump {inside foxtrot, v -> $w}
	global resolution
	set ::resolution $w
    }
    set v "global level"
} -body {
    set result [bravo]
    if {$result ne $v} {
	puts "v should have been found at $v but was found in $result"
    }
} -cleanup {
    unset v
    rename dump {}
    rename bravo {}
    rename delta {}
    rename foxtrot {}
} -output {1: inside bravo, v -> global level
1: inside delta, v -> global level
1: inside foxtrot, v -> global level
}

test tailcall-12.2 {[Bug 2649975]} -setup {
    proc dump {{text {}}} {
	set text [uplevel 1 [list subst $text]]
	set l [expr {[info level] -1}]
	if {$text eq {}} {
	    set text [info level $l]
	}
	puts "$l: $text"
    }
    # proc dump args {}
    set v "global level"
    oo::class create foo { # like connection
	method alpha {} {  # like connections 'tables' method
	    dump
	    upvar 1 v w
	    dump {inside foo's alpha, v resolves to $w}
	    set v "foo's method alpha"
	    dump {foo's alpha is calling [self] bravo - v should resolve at global level}
	    set result [uplevel 1 [list [self] bravo]]
	    dump {exiting from foo's alpha}
	    return $result
	}
	method bravo {} {  # like connections 'foreach' method
	    dump
	    upvar 1 v w
	    dump {inside foo's bravo, v resolves to $w}
	    set v "foo's method bravo"
	    dump {foo's bravo is calling charlie to create barney}
	    set barney [my charlie ::barney]
	    dump {foo's bravo is calling bravo on $barney}
	    dump {v should resolve at global scope there}
	    set result [uplevel 1 [list $barney bravo]]
	    dump {exiting from foo's bravo}
	    return $result
	}
	method charlie {name} {  # like tdbc prepare
	    dump
	    set v "foo's method charlie"
	    dump {tailcalling bar's constructor}
	    tailcall ::bar create $name
	}
    }
    oo::class create bar { # like statement
	method  bravo {} {   # like statement foreach method
	    dump
	    upvar 1 v w
	    dump {inside bar's bravo, v is resolving to $w}
	    set v "bar's method bravo"
	    dump {calling delta to construct betty - v should resolve global there}
	    uplevel 1 [list [self] delta ::betty]
	    dump {exiting from bar's bravo}
	    return [::betty whathappened]
	}
	method delta {name} {    # like statement execute method
	    dump
	    upvar 1 v w
	    dump {inside bar's delta, v is resolving to $w}
	    set v "bar's method delta"
	    dump {tailcalling to construct $name as instance of grill}
	    dump {v should resolve at global level in grill's constructor}
	    dump {grill's constructor should run at level [info level]}
	    tailcall grill create $name
	}
    }
    oo::class create grill {
	variable resolution
	constructor {} {
	    dump
	    upvar 1 v w
	    dump "in grill's constructor, v resolves to $w"
	    set resolution $w
	}
	method whathappened {} {
	    return $resolution
	}
    }
    foo create fred
} -body {
    set result [fred alpha]
    if {$result ne "global level"} {
	puts "v should have been found at global level but was found in $result"
    }
} -cleanup {
    unset result
    rename fred {}
    rename dump {}
    rename foo {}
    rename bar {}
    rename grill {}
} -output {1: fred alpha
1: inside foo's alpha, v resolves to global level
1: foo's alpha is calling ::fred bravo - v should resolve at global level
1: ::fred bravo
1: inside foo's bravo, v resolves to global level
1: foo's bravo is calling charlie to create barney
2: my charlie ::barney
2: tailcalling bar's constructor
1: foo's bravo is calling bravo on ::barney
1: v should resolve at global scope there
1: ::barney bravo
1: inside bar's bravo, v is resolving to global level
1: calling delta to construct betty - v should resolve global there
1: ::barney delta ::betty
1: inside bar's delta, v is resolving to global level
1: tailcalling to construct ::betty as instance of grill
1: v should resolve at global level in grill's constructor
1: grill's constructor should run at level 1
1: grill create ::betty
1: in grill's constructor, v resolves to global level
1: exiting from bar's bravo
1: exiting from foo's bravo
1: exiting from foo's alpha
}

test tailcall-12.3a0 {[Bug 2695587]} -body {
    apply {{} {
	catch [list tailcall foo]
    }}
} -returnCodes 1 -result {invalid command name "foo"}

test tailcall-12.3a1 {[Bug 2695587]} -body {
    apply {{} {
	catch [list tailcall foo]
	tailcall
    }}
} -result {}

test tailcall-12.3a2 {[Bug 2695587]} -body {
    apply {{} {
	catch [list tailcall foo]
	tailcall moo
    }}
} -returnCodes 1 -result {invalid command name "moo"}

test tailcall-12.3a3 {[Bug 2695587]} -body {
    set x 0
    apply {{} {
	catch [list tailcall foo]
	tailcall lappend x 1
    }}
    set x
} -cleanup {
    unset x
} -result {0 1}

test tailcall-12.3b0 {[Bug 2695587]} -body {
    apply {{} {
	set catch catch
	$catch [list tailcall foo]
    }}
} -returnCodes 1 -result {invalid command name "foo"}

test tailcall-12.3b1 {[Bug 2695587]} -body {
    apply {{} {
	set catch catch
	$catch [list tailcall foo]
	tailcall
    }}
} -result {}

test tailcall-12.3b2 {[Bug 2695587]} -body {
    apply {{} {
	set catch catch
	$catch [list tailcall foo]
	tailcall moo
    }}
} -returnCodes 1 -result {invalid command name "moo"}

test tailcall-12.3b3 {[Bug 2695587]} -body {
    set x 0
    apply {{} {
	set catch catch
	$catch [list tailcall foo]
	tailcall lappend x 1
    }}
    set x
} -cleanup {
    unset x
} -result {0 1}

# MORE VARIANTS MISSING: bc'ed caught script vs (bc'ed, not-bc'ed)
# catch. Actually superfluous now, as tailcall just returns TCL_RETURN so that
# standard catch behaviour is required.

test tailcall-13.1 {directly tailcalling the tailcall command is ok} {
    list [catch {
	apply {{} {
	    apply {{} {
		tailcall tailcall subst ok
		subst b
	    }}
	    subst c
	}}
    } msg opt] $msg [errorcode $opt]
} {0 ok NONE}
test tailcall-13.2 {indirectly tailcalling the tailcall command is ok} {
    list [catch {
	apply {{} {
	    apply {{} {
		tailcall eval tailcall subst ok
		subst b
	    }}
	    subst c
	}}
    } msg opt] $msg [errorcode $opt]
} {0 ok NONE}

if {[testConstraint testnrelevels]} {
    namespace forget testnre::*
    namespace delete testnre
}

test tailcall-14.1 {in a deleted namespace} -body {
    namespace eval ns {
	proc p args {
	    tailcall [namespace current] $args
	}
	namespace delete [namespace current]
	p
    }
} -returnCodes 1 -result {namespace "::ns" not found}

test tailcall-14.1-bc {{in a deleted namespace} {byte compiled}} -body {
    namespace eval ns {
	proc p args {
	    tailcall [namespace current] {*}$args
	}
	namespace delete [namespace current]
	p
    }
} -returnCodes 1 -result {namespace "::ns" not found}

# cleanup
::tcltest::cleanupTests

# Local Variables:
# mode: tcl
# End:
