# Copyright (C) 2017-2025 Free Software Foundation, Inc.

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GCC; see the file COPYING3.  If not see
# <http://www.gnu.org/licenses/>.
#
# Contributed by Nathan Sidwell <nathan@acm.org> while at Facebook


# Test C++ modules, which requires multiple TUs
#
# A test case might consist of multiple source files, each is compiled
# separately, in a well-defined order.  The resulting object files might
# be optionally linked and optionally executed.  Grouping is indicated by
# naming files '*_[a-z].[CH]'

# { dg-module-cmi "[!]module-name" } # an interface file is (not) expected
# { dg-module-do [link|run] [xfail] [options] } # link [and run]

load_lib g++-dg.exp
load_lib modules.exp

# If a testcase doesn't have special options, use these.
global DEFAULT_CXXFLAGS
if ![info exists DEFAULT_CXXFLAGS] then {
    set DEFAULT_CXXFLAGS " -pedantic-errors -Wno-long-long"
}
set DEFAULT_MODFLAGS $DEFAULT_CXXFLAGS
set MOD_STD_LIST { 17 2a 2b }

dg-init

if {[is_remote host]} {
    # remote testing not functional here :(
    return
}

if { [istarget "powerpc-ibm-aix*"] } {
    set torture_execute_xfail "powerpc-ibm-aix*"
    return
}

global module_do
global module_cmis

set DEFAULT_REPO "gcm.cache"

# Register the module name this produces.
# dg-module-cmi !?=?NAME WHEN?
# dg-module-cmi !?{} - header unit
proc dg-module-cmi { args } {
    if { [llength $args] > 3 } {
	error "[lindex $args 0]: too many arguments"
	return
    }
    set spec [lindex $args 1]
    if { [llength $args] > 2 } {
	set when [lindex $args 2]
    } else {
	set when {}
    }

    if { [string index $spec 0] == "!" } {
	set name [string range $spec 1 end]
	set not 1
    } else {
	set name $spec
	set not 0
    }

    if { [string index $name 0] == "=" } {
	set cmi [string range $name 1 end]
    } else {
	if { $name == "" } {
	    # get the source file name.  ick!
	    upvar prog srcname
	    set cmi "$srcname.gcm"
	    if { [string index $cmi 0] == "/" } {
		set cmi [string range $cmi 1 end]
	    } else {
		set cmi ",/$cmi"
	    }
	    set path [file split $cmi]
	    # subst /../ -> /,,/
	    # sadly tcl 8.5 does not have lmap
	    set rplac {}
	    foreach elt $path {lappend rplac [expr {$elt == ".." ? ",," : $elt}]}
	    set cmi [file join {*}$rplac]
	} else {
	    set cmi "[regsub : $name -].gcm"
	}
	global DEFAULT_REPO
	set cmi "$DEFAULT_REPO/$cmi"
    }

    # delete file, so we don't get confused by a stale one.
    file_on_host delete "$cmi"

    global module_cmis
    lappend module_cmis [list $spec $when $not $cmi]
}

# check the expected module files exist (or not)
# return list to delete
proc module_cmi_p { src ifs } {
    set res {}
    foreach if_arg $ifs {
	set spec [lindex $if_arg 0]
	set when [lindex $if_arg 1]
	if { $when != "" } {
	    switch [dg-process-target $when] {
		"S" { }
		"N" { continue }
		"F" { setup_xfail "*-*-*" }
		"P" { }
	    }
	}
	set not [lindex $if_arg 2]
	set cmi [lindex $if_arg 3]
	global srcdir
	set relcmi [string map [list $srcdir "/\$srcdir"] $cmi]
	if { $not != [file_on_host exists $cmi] } {
	    pass "$src module-cmi $spec ($relcmi)"
	} else {
	    fail "$src module-cmi $spec ($relcmi)"
	    set not [expr ! $not ]
	}
	if { ! $not } {
	    lappend res $cmi
	}
    }
    return $res
}

# link and maybe run a set of object files
# dg-module-do WHAT WHEN
proc dg-module-do { args } {
    if { [llength $args] > 3 } {
	error "[lindex $args 0]: too many arguments"
	return
    }

    set do_what [lindex $args 1]
    set expected "P"
    if { [llength $args] > 2 } {
	set expected [dg-process-target [lindex $args 2]]
    }

    global module_do
    set module_do [list $do_what $expected]
}

proc module_do_it { do_what testcase std asm_list } {
    global tool

    set run 0
    switch [lindex $do_what 0] {
	"compile" { return 1 }
	"link" { }
	"run" { set run 1 }
	default { error "unknown module-do action [lindex $do_what 0]" }
    }

    set xfail {}
    switch [lindex $do_what 1] {
	"S" { }
	"N" { return 1 }
	"F" { set xfail {setup_xfail "*-*-*"} }
	"P" { }
    }

    set ok 1
    # make sure all asms are around
    foreach asm $asm_list {
	if { ! [file_on_host exists $asm] } {
	    set ok 0
	}
    }

    set options { }
    set ident $testcase
    if { $std != "" } {
	lappend options "additional_flags=$std"
	set ident "$ident $std"
    }
    global extra_tool_flags
    if { [llength $extra_tool_flags] } {
	lappend options "additional_flags=$extra_tool_flags"
    }

    set execname "./[file tail $testcase].exe"

    # link it
    verbose "Linking $asm_list" 1
    if { !$ok } {
	unresolved "$ident link"
    } else {
	global target_triplet
	set out [${tool}_target_compile $asm_list \
		     $execname executable $options]
	eval $xfail

	# Do gcc-specific pruning.
	set out [${tool}-dg-prune $target_triplet $out]
	# Fix up remaining line-breaks similar to "regular" pruning
	# calls.  Otherwise, a multi-line message stripped e.g. one
	# part by the default prune_warnings and one part part by the
	# gcc prune_gcc_output will have a residual line-break.
	regsub "^\[\r\n\]+" $out "" out

	if { $out == "" } {
	    pass "$ident link"
	} else {
	    fail "$ident link"
	    set ok 0
	}
    }

    # run it?
    if { !$run } {
    } elseif { !$ok } {
	unresolved "$ident execute"
    } else {
	set out [${tool}_load $execname "" ""]
	set status [lindex $out 0]
	eval $xfail
	$status "$ident execute"
	if { $status != "pass" } {
	    set $ok 0
	}
    }

    if { $ok } {
	file_on_host delete $execname
    }

    return $ok
}

# delete the specified set of module files
proc cleanup_module_files { files } {
    foreach file $files {
	file_on_host delete $file
    }
}

global testdir
set testdir $srcdir/$subdir
proc srcdir {} {
    global testdir
    return $testdir
}

# Return set of std options to iterate over, taken from g++-dg.exp & compat.exp
proc module-init { src } {
    set tmp [dg-get-options $src]
    set option_list {}
    set have_std 0
    set std_prefix "-std=c++"
    global extra_tool_flags
    set extra_tool_flags {}
    global MOD_STD_LIST

    foreach op $tmp {
	switch [lindex $op 0] {
	    "dg-options" {
		set std_prefix "-std=gnu++"
		if { [string match "*-std=*" [lindex $op 2]] } {
		    set have_std 1
		}
		eval lappend extra_tool_flags [lindex $op 2]
	    }
	    "dg-additional-options" {
		if { [string match "*-std=*" [lindex $op 2]] } {
		    set have_std 1
		}
		eval lappend extra_tool_flags [lindex $op 2]
	    }
	}
    }

    if { $have_std } {
	lappend option_list ""
    } elseif { [string match "*xtreme*" $src] } {
	# Only run the xtreme tests once.
	set x [lindex $MOD_STD_LIST end]
	lappend option_list "${std_prefix}$x"
    } else {
	foreach x $MOD_STD_LIST {
	    lappend option_list "${std_prefix}$x"
	}
    }

    return $option_list
}

# Return 1 if requirements are met
proc module-check-requirements { tests } {
    foreach test $tests {
	set tmp [dg-get-options $test]
	foreach op $tmp {
	    switch [lindex $op 0] {
		"dg-additional-options" {
		    # Example strings to match:
		    # -fmodules-ts -fmodule-mapper=|@g++-mapper-server\\ -t\\ [srcdir]/inc-xlate-1.map
		    # -fmodules-ts -fmodule-mapper=|@g++-mapper-server
		    if [regexp -- {(^| )-fmodule-mapper=\|@([^\\ ]*)} [lindex $op 2] dummy dummy2 prog] {
			verbose "Checking that mapper exist: $prog"
			if { ![ check_is_prog_name_available $prog ] } {
			    return 0
			}
		    }
		}
	    }
	}
    }
    return 1
}

# cleanup any detritus from previous run
cleanup_module_files [find $DEFAULT_REPO *.gcm]

# Override unsupported to set the second element of module_do to "N",
# so that, after an unsupported result in dg-test, we can skip rather
# than fail subsequent related tests.
set module_do {"compile" "P"}
rename unsupported modules-saved-unsupported
proc unsupported { args } {
    global module_do
    lset module_do 1 "N"
    return [uplevel 1 modules-saved-unsupported $args]
}

# not grouped tests, sadly tcl doesn't have negated glob
foreach test [prune [lsort [find $srcdir/$subdir {*.[CH]}]] \
		  "$srcdir/$subdir/*_?.\[CH\]"] {
    if [runtest_file_p $runtests $test] {
	set nshort [file tail [file dirname $test]]/[file tail $test]

	set std_list [module-init $test]
	foreach std $std_list {
	    global module_cmis
	    set module_cmis {}
	    verbose "Testing $nshort $std" 1
	    dg-test $test "$std" $DEFAULT_MODFLAGS
	    if { [lindex $module_do 1] == "N" } {
		continue
	    }
	    set testcase [string range $test [string length "$srcdir/"] end]
	    cleanup_module_files [module_cmi_p $testcase $module_cmis]
	}
    }
}

# grouped tests
foreach src [lsort [find $srcdir/$subdir {*_a.[CHX}]] {
    # use the FOO_a.C name as the parallelization key
    if [runtest_file_p $runtests $src] {
	set tests [lsort [find [file dirname $src] \
			      [regsub {_a.[CHX]$} [file tail $src] {_[a-z].[CHX]}]]]

	if { ![module-check-requirements $tests] } {
	    set testcase [regsub {_a.[CH]} $src {}]
	    set testcase \
		[string range $testcase [string length "$srcdir/"] end]
	    unsupported $testcase
	    continue
	}

	set std_list [module-init $src]
	foreach std $std_list {
	    set mod_files {}
	    global module_do
	    set module_do {"compile" "P"}
	    set asm_list {}
	    set any_hdrs 0
	    global DEFAULT_REPO
	    file_on_host delete $DEFAULT_REPO
	    foreach test $tests {
		if { [lindex $module_do 1] != "N" } {
		    global module_cmis
		    set module_cmis {}
		    set nshort [file tail [file dirname $test]]/[file tail $test]
		    verbose "Testing $nshort $std" 1
		    switch [file extension $test] {
			".C" {		
			    lappend asm_list [file rootname [file tail $test]].s
			}
			".X" {
			    set any_hdrs 1
			}
		    }
		    dg-test -keep-output $test "$std" $DEFAULT_MODFLAGS
		    if { [lindex $module_do 1] == "N" } {
			break
		    }
		    set testcase [string range $test [string length "$srcdir/"] end]
		    lappend mod_files [module_cmi_p $testcase $module_cmis]
		}
	    }
	    set testcase [regsub {_a.[CH]} $src {}]
	    set testcase \
		[string range $testcase [string length "$srcdir/"] end]
	    module_do_it $module_do $testcase $std $asm_list
	    foreach asm $asm_list {
		file_on_host delete $asm
	    }
	    if { $any_hdrs } {
		set mod_files [find $DEFAULT_REPO *.gcm]
	    }
	    cleanup_module_files $mod_files
	}
    }
}

# Restore the original unsupported proc, lest it will affect
# subsequent test runs, or even fail renaming if we run modules.exp
# for multiple targets/multilibs/options.
rename unsupported {}
rename modules-saved-unsupported unsupported

dg-finish