summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--generic/tclBasic.c3
-rw-r--r--generic/tclCompCmdsSZ.c53
-rw-r--r--generic/tclCompile.c5
-rw-r--r--generic/tclCompile.h1
-rw-r--r--generic/tclExecute.c49
-rw-r--r--tests/tailcall.test70
6 files changed, 177 insertions, 4 deletions
diff --git a/generic/tclBasic.c b/generic/tclBasic.c
index afc17c5..0d9cd89 100644
--- a/generic/tclBasic.c
+++ b/generic/tclBasic.c
@@ -363,7 +363,7 @@ static const CmdInfo builtInCmds[] = {
{"split", Tcl_SplitObjCmd, NULL, NULL, CMD_IS_SAFE},
{"subst", Tcl_SubstObjCmd, TclCompileSubstCmd, TclNRSubstObjCmd, CMD_IS_SAFE},
{"switch", Tcl_SwitchObjCmd, TclCompileSwitchCmd, TclNRSwitchObjCmd, CMD_IS_SAFE},
- {"tailcall", NULL, TclCompileTailcallCmd, TclNRTailcallObjCmd, CMD_IS_SAFE},
+ {"tailcall", NULL, TclCompileTailcallCmd, TclNRTailcallObjCmd, CMD_IS_SAFE|CMD_COMPILES_EXPANDED},
{"throw", Tcl_ThrowObjCmd, TclCompileThrowCmd, NULL, CMD_IS_SAFE},
{"trace", Tcl_TraceObjCmd, NULL, NULL, CMD_IS_SAFE},
{"try", Tcl_TryObjCmd, TclCompileTryCmd, TclNRTryObjCmd, CMD_IS_SAFE},
@@ -8817,6 +8817,7 @@ TclNRTailcallObjCmd(
listPtr = Tcl_NewListObj(objc, objv);
TclListObjSetElement(NULL, listPtr, 0, TclNewNamespaceObj(nsPtr));
+ Tcl_IncrRefCount(listPtr);
iPtr->varFramePtr->tailcallPtr = listPtr;
}
diff --git a/generic/tclCompCmdsSZ.c b/generic/tclCompCmdsSZ.c
index 41bc866..da6d2d0 100644
--- a/generic/tclCompCmdsSZ.c
+++ b/generic/tclCompCmdsSZ.c
@@ -2697,11 +2697,64 @@ TclCompileTailcallCmd(
OP( NS_CURRENT);
for (i=1 ; i<numWords ; i++) {
tokenPtr = TokenAfter(tokenPtr);
+ if (tokenPtr->type == TCL_TOKEN_EXPAND_WORD) {
+ goto tailcallExpanded;
+ }
+ }
+ tokenPtr = parsePtr->tokenPtr;
+
+ for (i=1 ; i<numWords ; i++) {
+ tokenPtr = TokenAfter(tokenPtr);
// TODO: If the first token is a literal, add LITERAL_CMD_NAME to its flags
PUSH_TOKEN( tokenPtr, i);
}
OP4( TAILCALL, numWords);
return TCL_OK;
+
+ tailcallExpanded:
+ {
+ Tcl_Size build = 0;
+ int concat = 0;
+
+ tokenPtr = parsePtr->tokenPtr;
+ for (i = 1; i < numWords; i++) {
+ tokenPtr = TokenAfter(tokenPtr);
+ if (tokenPtr->type == TCL_TOKEN_EXPAND_WORD && build > 0) {
+ OP4( LIST, build);
+ if (concat) {
+ OP( LIST_CONCAT);
+ }
+ build = 0;
+ concat = 1;
+ }
+ PUSH_TOKEN( tokenPtr, i);
+ if (tokenPtr->type == TCL_TOKEN_EXPAND_WORD) {
+ if (concat) {
+ OP( LIST_CONCAT);
+ } else {
+ concat = 1;
+ }
+ } else {
+ build++;
+ }
+ if (build > (1 << 12)) {
+ OP4( LIST, build);
+ if (concat) {
+ OP( LIST_CONCAT);
+ }
+ build = 0;
+ concat = 1;
+ }
+ }
+ if (build > 0) {
+ OP4( LIST, build);
+ if (concat) {
+ OP( LIST_CONCAT);
+ }
+ }
+ }
+ OP( TAILCALL_LIST);
+ return TCL_OK;
}
/*
diff --git a/generic/tclCompile.c b/generic/tclCompile.c
index a3008c5..6e2a161 100644
--- a/generic/tclCompile.c
+++ b/generic/tclCompile.c
@@ -955,6 +955,11 @@ InstructionDesc const tclInstructionTable[] = {
* Stack: ... value => ...
* Note that the jump table contains offsets relative to the PC when
* it points to this instruction; the code is relocatable. */
+ TCL_INSTRUCTION_ENTRY(
+ "tailcallList", -1),
+ /* Do a tailcall with the words from wordList as the thing to
+ * tailcall to, and currNs is the namespace scope.
+ * Stack: ... currNs wordList => ...[NOT REACHED] */
{NULL, 0, 0, 0, {OPERAND_NONE}}
};
diff --git a/generic/tclCompile.h b/generic/tclCompile.h
index 1c184d3..4d68732 100644
--- a/generic/tclCompile.h
+++ b/generic/tclCompile.h
@@ -931,6 +931,7 @@ enum TclInstruction {
INST_DICT_REMOVE,
INST_IS_EMPTY,
INST_JUMP_TABLE_NUM,
+ INST_TAILCALL_LIST,
/* The last opcode */
LAST_INST_OPCODE
diff --git a/generic/tclExecute.c b/generic/tclExecute.c
index 9a1d8cb..d3712bd 100644
--- a/generic/tclExecute.c
+++ b/generic/tclExecute.c
@@ -2576,8 +2576,7 @@ TEBCresume(
}
#ifdef TCL_COMPILE_DEBUG
- /* FIXME: What is the right thing to trace? */
- {
+ if (tclTraceExec >= TCL_TRACE_BYTECODE_EXEC_COMMANDS) {
Tcl_Size i;
TRACE(("%u [", (unsigned) numArgs));
@@ -2587,7 +2586,7 @@ TEBCresume(
TRACE_APPEND((" "));
}
}
- TRACE_APPEND(("] => RETURN...\n"));
+ TRACE_APPEND(("] => REGISTERED TAILCALL...\n"));
}
#endif
@@ -2609,12 +2608,56 @@ TEBCresume(
Tcl_DecrRefCount(iPtr->varFramePtr->tailcallPtr);
}
iPtr->varFramePtr->tailcallPtr = listPtr;
+ Tcl_IncrRefCount(listPtr);
result = TCL_RETURN;
cleanup = numArgs;
goto processExceptionReturn;
}
+ case INST_TAILCALL_LIST:
+ if (!(iPtr->varFramePtr->isProcCallFrame & 1)) {
+ TRACE((" => ERROR: tailcall in non-proc context\n"));
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(
+ "tailcall can only be called from a proc or lambda", -1));
+ DECACHE_STACK_INFO();
+ Tcl_SetErrorCode(interp, "TCL", "TAILCALL", "ILLEGAL", (char *)NULL);
+ CACHE_STACK_INFO();
+ goto gotError;
+ }
+
+ {
+ Tcl_Obj *listPtr = OBJ_AT_TOS;
+ // nsPtr = OBJ_UNDER_TOS; // Don't need this variable
+#ifdef TCL_COMPILE_DEBUG
+ if (tclTraceExec >= TCL_TRACE_BYTECODE_EXEC_COMMANDS) {
+ TRACE(("%s [", O2S(OBJ_UNDER_TOS)));
+ TclPrintObject(stdout, listPtr, 40);
+ TRACE_APPEND(("] => REGISTERED TAILCALL...\n"));
+ }
+#endif
+
+ /*
+ * Push the evaluation of the called command into the NR callback
+ * stack.
+ */
+
+ if (iPtr->varFramePtr->tailcallPtr) {
+ Tcl_DecrRefCount(iPtr->varFramePtr->tailcallPtr);
+ }
+ if (Tcl_IsShared(listPtr)) {
+ listPtr = Tcl_DuplicateObj(listPtr);
+ }
+ // TODO: Consider requiring a blank or the NS at the start of the list.
+ Tcl_ListObjReplace(NULL, listPtr, 0, 0, 1, &OBJ_UNDER_TOS);
+ Tcl_IncrRefCount(listPtr);
+ iPtr->varFramePtr->tailcallPtr = listPtr;
+
+ result = TCL_RETURN;
+ cleanup = 2;
+ goto processExceptionReturn;
+ }
+
case INST_DONE:
if (tosPtr > initTosPtr) {
diff --git a/tests/tailcall.test b/tests/tailcall.test
index 0016845..6edf8b8 100644
--- a/tests/tailcall.test
+++ b/tests/tailcall.test
@@ -49,6 +49,24 @@ if {[testConstraint testnrelevels]} {
proc errorcode options {
dict get [dict merge {-errorcode NONE} $options] -errorcode
}
+
+# Used for constraining memory leak tests
+testConstraint memory [llength [info commands memory]]
+if {[testConstraint memory]} {
+ proc getbytes {} {
+ set lines [split [memory info] \n]
+ return [lindex $lines 3 3]
+ }
+ proc leaktest {script {iterations 3}} {
+ set end [getbytes]
+ for {set i 0} {$i < $iterations} {incr i} {
+ uplevel 1 $script
+ set tmp $end
+ set end [getbytes]
+ }
+ return [expr {$end - $tmp}]
+ }
+}
test tailcall-0.1 {tailcall is constant space} -constraints testnrelevels -setup {
proc a i {
@@ -708,6 +726,58 @@ test tailcall-14.1-bc {{in a deleted namespace} {byte compiled}} -body {
}
} -returnCodes 1 -result {namespace "::ns" not found}
+test tailcall-15.1 {tailcall memory leak check} -constraints memory -setup {
+ proc foo {args} {llength $args}
+} -body {
+ list [
+ apply {cmd {
+ $cmd foo 1 2 3 4 5
+ }} tailcall
+ ] [
+ leaktest {
+ apply {cmd {
+ $cmd foo 1 2 3 4 5
+ }} tailcall
+ }
+ ]
+} -cleanup {
+ rename foo {}
+} -result {5 0}
+test tailcall-15.2 {tailcall memory leak check} -constraints memory -setup {
+ proc foo {args} {llength $args}
+} -body {
+ list [
+ apply {{} {
+ tailcall foo 1 2 3 4 5
+ }}
+ ] [
+ leaktest {
+ apply {{} {
+ tailcall foo 1 2 3 4 5
+ }}
+ }
+ ]
+} -cleanup {
+ rename foo {}
+} -result {5 0}
+test tailcall-15.3 {tailcall memory leak check} -constraints memory -setup {
+ proc foo {args} {llength $args}
+} -body {
+ list [
+ apply {args {
+ tailcall foo 1 2 {*}$args 3 4 {*}$args 5
+ }} a b c
+ ] [
+ leaktest {
+ apply {args {
+ tailcall foo 1 2 {*}$args 3 4 {*}$args 5
+ }} a b c
+ }
+ ]
+} -cleanup {
+ rename foo {}
+} -result {11 0}
+
test tailcall-bug-784befb0ba {tailcall crash with 254 args} -body {
proc tccrash args {llength $args}
# Must be EXACTLY 254 for crash