diff --git a/doc/en/CAS/Simplification.md b/doc/en/CAS/Simplification.md index f915a8f6d8..2c73980197 100644 --- a/doc/en/CAS/Simplification.md +++ b/doc/en/CAS/Simplification.md @@ -230,7 +230,7 @@ One problem is that `makelist` needs simplification. To create sequences/series S1:ev(makelist(k,k,1,N),simp) S2:maplist(lambda([ex],ev(an,n=ex)),S1) S3:ev(S2,simp) - S4apply("+",S3) + S4:apply("+",S3) Of course, to print out one line in the worked solution you can also `apply("+",S2)` as well. @@ -276,6 +276,23 @@ Maxima does have the ability to make assumptions, e.g. to assume that \(n\) is a The variable `sans1` can then be used in the PRT. Just note that `trigrat` writes powers of trig functions in terms of multiple angles. This can have an effect of "expanding" out an expression. E.g. `trigrat(cos(n)^20)` is probably still fine, but `trigrat(cos(n)^2000)` is probably not! For this reason `trigrat` is not part of the default routines to establish equivalence. Trig simplification, especially when we make assumptions on variables like \(n\), needs to be done on a question by question basis. +## Function identifies which are compound quantities. + +Typically operators should be single identifiers, e.g. with \(f\) applied to \(x\) in \(f(x)\) the identifier is simple. Maxima supports compound operators, e.g. `(X+1)(x,y,z);` is valid Maxima. +This syntax is problematic, and typically results from a user error, e.g. of the following kind in question variables. + +``` +a:b+1; +c:a-a(d+1); +``` + +We now have `a` as a variable and a function name. By default, STACK restricts the ability of users to apply compound function identifiers (it is normally signified by an error). However, for very advanced cases they can be useful. + +This option decides if we will allow application of compound operators. It can use used in the question variables. + +`OPT_APPLY_COMPOUND:false;` + + ## Boolean functions See the page on [propositional logic](../Topics/Propositional_Logic.md). diff --git a/doc/en/Developer/Development_track.md b/doc/en/Developer/Development_track.md index bf99e4f691..23adc1a582 100644 --- a/doc/en/Developer/Development_track.md +++ b/doc/en/Developer/Development_track.md @@ -12,6 +12,7 @@ DONE. 1. Add in the ability to insert stars for "unknown functions" in inputs. E.g. `x(t+1)` becomes `x*(t+1)`. This only affects "unknown" functions, not core functions such as `sin(x)`. 2. Add in tags to the `[[todo]]` blocks to help with multi-authoring [workflow](../Authoring/Workflow.md). 3. Add in a library page which allows users to load question from the sample question folder on the server. This gives users ready access to openly released sample materials. +4. Add in the option `OPT_APPLY_COMPOUND` to control whenn STACK accepts application of compound identifiers as function names. Issues with [github milestone 4.8.0](https://github.com/maths/moodle-qtype_stack/issues?q=is%3Aissue+milestone%3A4.8.0) include diff --git a/stack/cas/cassession2.class.php b/stack/cas/cassession2.class.php index 5805b89684..9bdec031de 100644 --- a/stack/cas/cassession2.class.php +++ b/stack/cas/cassession2.class.php @@ -410,10 +410,6 @@ public function instantiate(): bool { $asts[$key] = $value; } } catch (Exception $e) { - // TODO: issue #1279 would change this exception to add in an error associated - // with the values collected rather than a stack_exception. - // We would then add something like this to allow the process to continue. - // $asts[$key] = maxima_parser_utils::parse('null', 'Root', false); throw new stack_exception('stack_cas_session: tried to parse the value ' . $value . ', but got the following exception ' . $e->getMessage()); } diff --git a/stack/cas/security-map.json b/stack/cas/security-map.json index fedfb8eba1..a710c7ed7f 100644 --- a/stack/cas/security-map.json +++ b/stack/cas/security-map.json @@ -56,6 +56,10 @@ "globalyforbiddenvariable": true, "variable": "f" }, + "OPT_APPLY_COMPOUND": { + "variable": "t", + "contextvariable": "true" + }, "%and": { "operator": "s" }, diff --git a/stack/maxima/contrib/prooflib.mac b/stack/maxima/contrib/prooflib.mac index d1e66edd51..b6d1e51d09 100644 --- a/stack/maxima/contrib/prooflib.mac +++ b/stack/maxima/contrib/prooflib.mac @@ -273,189 +273,3 @@ dispproof([ex]) := block([ex1], ); */ -/******************************************************************/ -/* */ -/* Assessment and feedback functions */ -/* */ -/******************************************************************/ - -/* ********************************** */ -/* Levenshtein distance */ -/* ********************************** */ - -/* - Levenshtein distance with swap tracking - s,t: lists to compare - Returns integer d, the Levensthein distance between s and t. - Returns the process of getting from s to t. - Original author Achim Eichhorn Achim.Eichhorn(at)hs-esslingen.de modified by Chris Sangwin to track process. -*/ -proof_damerau_levenstein(s, t) := block([c, m, n, XY, XYaction, i, j, d, temp, L, lm, li, dl_tags, simp], - simp:true, - if(s=t) then return([0,[]]), /* Equal strings result in 0, nothing to do. */ - m:length(s), - n:length(t), - XY: matrix(makelist(i,i,0,n), makelist(0,i,1,n+1)), - XYaction: matrix(makelist(makelist(dl_add(t[k]),k,1,i),i,0,n), makelist([],i,1,n+1)), - for i:1 thru m do ( - XY[2][1]:i, - XYaction[2][1]:makelist(dl_delete(s[k]),k,1,i), - for j:1 thru n do( - c:if is(s[i]=t[j]) then 0 else 1, - L:[XY[2][j]+1, /* Insertion */ - XY[1][j+1]+1, /* Deletion */ - XY[1][j]+c], /* Substitution */ - /* Add in the swap rule. */ - /* The swapping costs nothing, but the cost comes from the subsequent dl_subs, which we filter out. */ - if is(i ", ex); -dl_ok_disp(ex) := ""; -dl_delete_disp(ex) := ""; -dl_swap_disp([ex]) := ""; -dl_swap_follow_disp(ex) := ""; -dl_subs_disp([ex]) := sconcat(" ", - " ", second(ex)); - -proof_line_disp(ex1, ex2):= sconcat("
", ex1, ex2, "
"); -proof_comment_disp(ex):= sconcat("
", ex, "
"); -proof_column_disp(ex):= sconcat("
", ex, "
"); -proof_column_disp2(ex):= sconcat("
", ex, "
"); - -dl_disp(ex):=ev(ex, dl_empty=dl_empty_disp, dl_ok=dl_ok_disp, dl_delete=dl_delete_disp, dl_add=dl_add_disp, - dl_swap=dl_swap_disp, dl_swap_follow=dl_swap_follow_disp, dl_subs=dl_subs_disp); - -proof_assessment_display(saa, pf) := block([st, k], - /* An empty list is returned when we have a correct proof. */ - if emptyp(saa) then return(""), - saa:proof_disp_replacesteps(saa, pf), - /* sal is now a list of strings from the proof. */ - st:[], - for k:1 thru length(saa) do block([s0,s1], - s0:saa[k], - s1:first(s0), - if is(op(s0)=dl_add) then - st:append(st, [[dl_empty(null), s0]]) - else - st:append(st, [[s1, s0]]) - ), - /* Turn the st list of lists into a string to display. */ - st:dl_disp(st), - for k:1 thru length(saa) do block( - st[k]:proof_line_disp(proof_column_disp(first(st[k])), proof_column_disp(second(st[k]))) - ), - st:apply(sconcat, st), - sconcat("
", st, "
") -); - -/* ********************************** */ -/* Bespoke graph */ -/* ********************************** */ - -/* -For example, in this proof - -proof_steps: [ - ["H1", "This step is not needed in the proof."], - ["S1", "Assume that \\(3 \\cdot 2^{172} + 1\\) is a perfect square."], - ["S2", "There is a positive integer \\(k\\) such that \\(3 \\cdot 2^{172} + 1 = k^2\\)."], - ["S3", "Since \\(3 \\cdot 2^{172} + 1 > 2^{172} = (2^{86})^2 > 172^2\\), we have \\(k > 172\\)."], - ["S4", "We have \\(k^2 = 3 \\cdot 2^{172} + 1 < 3 \\cdot 2^{172} + 173\\)."], - ["S5", "Also, \\(3 \\cdot 2^{172} + 173 = (3 \\cdot 2^{172} + 1) + 172 < k^2 + k\\). Further, \\(k^2 + k < (k + 1)^2\\)."], - ["C6", "Since \\(k^2 < 3 \\cdot 2^{172} + 173 < (k + 1)^2\\) it is strictly between two successive squares \\(k^2\\) and \\((k + 1)^2\\), it cannot be a perfect square."] -]; - -we have two possible "interleaved" answers. - -sa1:["S1", "S2", "S3", "S4", "S5", "C6"]; -sa2:["S1", "S2", "S3", "S5", "S4", "C6"]; - -These can be defined as a directed acyclic graph. - -tdag: [ - ["S1", "S2", "S3", "S5", "C6"], - ["S1", "S2", "S4", "C6"] - ]; -*/ - -/* -This function checks if the student's answer (sa, a list) -has the dependencies specified in the teachers graph (tdag). - -It returns a list of "problems" in the form of - 1. Missing steps. - 2. Unnecessary steps. - 3. Order problems. -*/ -proof_assessment_dag(sa, tdag) := block([ttags,stags,proof_problems], - ttags:setify(flatten(tdag)), - if safe_op(sa)="proof" then sa:args(sa), - stags:setify(flatten(sa)), - proof_problems:[], - /* Each key used in the teacher's proof must occur in the student's list. */ - if not(subsetp(stags, ttags)) then proof_problems:[proof_step_extra(setdifference(stags, ttags))], - /* Only keys used in the teacher's proof should occur in the student's list. */ - if not(subsetp(ttags, stags)) then proof_problems:append(proof_problems,[proof_step_missing(setdifference(ttags, stags))]), - /* For each list, we check that the keys occur in the specified order in the student's proof. */ - proof_problems:flatten(append(proof_problems, map(lambda([ex], proof_dag_check_list(sa, ex)), tdag))), - /* Remove any duplicate problems. */ - listify(setify(proof_problems)) -)$ - -/* - Takes a list "l1", and makes sure sa respects the order of things in l1. -*/ -proof_dag_check_list(sa, l1) := block([li1,li2,m1], - if is(length(l1) < 2) then return([]), - m1:[], - /* By design we only check adjacent elements in the list. */ - li1:sublist_indices(sa, lambda([ex], ex=first(l1))), - li2:sublist_indices(sa, lambda([ex], ex=second(l1))), - /* Check if both steps appear. */ - if not(emptyp(li1) or emptyp(li2)) and apply(max, li1) > apply(min,li2) then m1:[proof_step_must(first(l1), second(l1))], - append(m1, proof_dag_check_list(sa, rest(l1))) -)$ \ No newline at end of file diff --git a/stack/maxima/proof.mac b/stack/maxima/proof.mac index 08f57328fc..777de8fe6d 100644 --- a/stack/maxima/proof.mac +++ b/stack/maxima/proof.mac @@ -422,3 +422,190 @@ match_answer(ans, steps, [rows]) := block([akeys, ukeys], * Alias for for the column-only usage `match_answer(ans_steps)` of `match_answer`. */ group_answer(ans, steps) := match_answer(ans, steps); + +/******************************************************************/ +/* */ +/* Assessment and feedback functions */ +/* */ +/******************************************************************/ + +/* ********************************** */ +/* Levenshtein distance */ +/* ********************************** */ + +/* + Levenshtein distance with swap tracking + s,t: lists to compare + Returns integer d, the Levensthein distance between s and t. + Returns the process of getting from s to t. + Original author Achim Eichhorn Achim.Eichhorn(at)hs-esslingen.de modified by Chris Sangwin to track process. +*/ +proof_damerau_levenstein(s, t) := block([c, m, n, XY, XYaction, i, j, d, temp, L, lm, li, dl_tags, simp], + simp:true, + if(s=t) then return([0,[]]), /* Equal strings result in 0, nothing to do. */ + m:length(s), + n:length(t), + XY: matrix(makelist(i,i,0,n), makelist(0,i,1,n+1)), + XYaction: matrix(makelist(makelist(dl_add(t[k]),k,1,i),i,0,n), makelist([],i,1,n+1)), + for i:1 thru m do ( + XY[2][1]:i, + XYaction[2][1]:makelist(dl_delete(s[k]),k,1,i), + for j:1 thru n do( + c:if is(s[i]=t[j]) then 0 else 1, + L:[XY[2][j]+1, /* Insertion */ + XY[1][j+1]+1, /* Deletion */ + XY[1][j]+c], /* Substitution */ + /* Add in the swap rule. */ + /* The swapping costs nothing, but the cost comes from the subsequent dl_subs, which we filter out. */ + if is(i ", ex); +dl_ok_disp(ex) := ""; +dl_delete_disp(ex) := ""; +dl_swap_disp([ex]) := ""; +dl_swap_follow_disp(ex) := ""; +dl_subs_disp([ex]) := sconcat(" ", + " ", second(ex)); + +proof_line_disp(ex1, ex2):= sconcat("
", ex1, ex2, "
"); +proof_comment_disp(ex):= sconcat("
", ex, "
"); +proof_column_disp(ex):= sconcat("
", ex, "
"); +proof_column_disp2(ex):= sconcat("
", ex, "
"); + +dl_disp(ex):=ev(ex, dl_empty=dl_empty_disp, dl_ok=dl_ok_disp, dl_delete=dl_delete_disp, dl_add=dl_add_disp, + dl_swap=dl_swap_disp, dl_swap_follow=dl_swap_follow_disp, dl_subs=dl_subs_disp); + +proof_assessment_display(saa, pf) := block([st, k], + /* An empty list is returned when we have a correct proof. */ + if emptyp(saa) then return(""), + saa:proof_disp_replacesteps(saa, pf), + /* sal is now a list of strings from the proof. */ + st:[], + for k:1 thru length(saa) do block([s0,s1], + s0:saa[k], + s1:first(s0), + if is(op(s0)=dl_add) then + st:append(st, [[dl_empty(null), s0]]) + else + st:append(st, [[s1, s0]]) + ), + /* Turn the st list of lists into a string to display. */ + st:dl_disp(st), + for k:1 thru length(saa) do block( + st[k]:proof_line_disp(proof_column_disp(first(st[k])), proof_column_disp(second(st[k]))) + ), + st:apply(sconcat, st), + sconcat("
", st, "
") +); + +/* ********************************** */ +/* Bespoke graph */ +/* ********************************** */ + +/* +For example, in this proof + +proof_steps: [ + ["H1", "This step is not needed in the proof."], + ["S1", "Assume that \\(3 \\cdot 2^{172} + 1\\) is a perfect square."], + ["S2", "There is a positive integer \\(k\\) such that \\(3 \\cdot 2^{172} + 1 = k^2\\)."], + ["S3", "Since \\(3 \\cdot 2^{172} + 1 > 2^{172} = (2^{86})^2 > 172^2\\), we have \\(k > 172\\)."], + ["S4", "We have \\(k^2 = 3 \\cdot 2^{172} + 1 < 3 \\cdot 2^{172} + 173\\)."], + ["S5", "Also, \\(3 \\cdot 2^{172} + 173 = (3 \\cdot 2^{172} + 1) + 172 < k^2 + k\\). Further, \\(k^2 + k < (k + 1)^2\\)."], + ["C6", "Since \\(k^2 < 3 \\cdot 2^{172} + 173 < (k + 1)^2\\) it is strictly between two successive squares \\(k^2\\) and \\((k + 1)^2\\), it cannot be a perfect square."] +]; + +we have two possible "interleaved" answers. + +sa1:["S1", "S2", "S3", "S4", "S5", "C6"]; +sa2:["S1", "S2", "S3", "S5", "S4", "C6"]; + +These can be defined as a directed acyclic graph. + +tdag: [ + ["S1", "S2", "S3", "S5", "C6"], + ["S1", "S2", "S4", "C6"] + ]; +*/ + +/* +This function checks if the student's answer (sa, a list) +has the dependencies specified in the teachers graph (tdag). + +It returns a list of "problems" in the form of + 1. Missing steps. + 2. Unnecessary steps. + 3. Order problems. +*/ +proof_assessment_dag(sa, tdag) := block([ttags,stags,proof_problems], + ttags:setify(flatten(tdag)), + if safe_op(sa)="proof" then sa:args(sa), + stags:setify(flatten(sa)), + proof_problems:[], + /* Each key used in the teacher's proof must occur in the student's list. */ + if not(subsetp(stags, ttags)) then proof_problems:[proof_step_extra(setdifference(stags, ttags))], + /* Only keys used in the teacher's proof should occur in the student's list. */ + if not(subsetp(ttags, stags)) then proof_problems:append(proof_problems,[proof_step_missing(setdifference(ttags, stags))]), + /* For each list, we check that the keys occur in the specified order in the student's proof. */ + proof_problems:flatten(append(proof_problems, map(lambda([ex], proof_dag_check_list(sa, ex)), tdag))), + /* Remove any duplicate problems. */ + listify(setify(proof_problems)) +)$ + +/* + Takes a list "l1", and makes sure sa respects the order of things in l1. +*/ +proof_dag_check_list(sa, l1) := block([li1,li2,m1], + if is(length(l1) < 2) then return([]), + m1:[], + /* By design we only check adjacent elements in the list. */ + li1:sublist_indices(sa, lambda([ex], ex=first(l1))), + li2:sublist_indices(sa, lambda([ex], ex=second(l1))), + /* Check if both steps appear. */ + if not(emptyp(li1) or emptyp(li2)) and apply(max, li1) > apply(min,li2) then m1:[proof_step_must(first(l1), second(l1))], + append(m1, proof_dag_check_list(sa, rest(l1))) +)$ diff --git a/stack/maxima/stackmaxima.mac b/stack/maxima/stackmaxima.mac index 90007bd5e0..7bb93c3ae9 100644 --- a/stack/maxima/stackmaxima.mac +++ b/stack/maxima/stackmaxima.mac @@ -200,6 +200,12 @@ declare ("#", commutative); simplify(ex) := ev(fullratsimp(ex), simp); /* Allows simplify to be something. */ degree(ex, v) := ev(hipow(expand(ex), v), simp); /* See notes on hipow. */ +/* This option decides if we will allow application of compound operators. + Typically operators should be single identifiers, e.g. f applied to x: f(x). + Maxima supports compound operators, e.g. (X+1)(x,y,z); is valid Maxima. +*/ +OPT_APPLY_COMPOUND:false; + make_complexJ(OPT_COMPLEXJ) := block( if OPT_COMPLEXJ = "i" then (texput(%i,"\\mathrm{i}")) @@ -649,8 +655,12 @@ _APPEND_FEEDBACK(feedback) := if ev(stackmap_has_key(%FEEDBACK, %stmt), simp) /* Reset any feedback. */ _RESET_FEEDBACK() := %FEEDBACK:stackmap_unset(%FEEDBACK,%stmt)$ -/* General error catching wrapper */ +/* General error catching wrapper, with check for non-atomic identifiers generated. */ _EC(errcatched, reference) := if errcatched = [] + then + (_APPEND_ERR([errormsgtostring()], reference), false) + else + if errcatch(all_ops_idp(errcatched)) = [] then (_APPEND_ERR([errormsgtostring()], reference), false) else @@ -665,6 +675,10 @@ _EC(errcatched, reference) := if errcatched = [] a list just in case there is an error of some sort, if it is not a list then that append would do bad things. */ _CS2v(_k,_v) := block([%_tmp], + /* Rather than use this error checking here, we've modified the output of grind's msz-mqapply + if errcatch(all_ops_idp(_v)) = [] + then _v:STACKERROR, + */ %_tmp:[[_k, string(_v)]], if listp(%_tmp) then _VALUES:append(_VALUES,%_tmp), 0)$ @@ -675,6 +689,10 @@ _CS2l(_k,_v) := block([%_tmp], _CS2dv(_k,_v) := block([%_tmp, simp], /* We don't want to simplify products with zero to zero here. */ simp:false, + /* Rather than use this error checking here, we've modified the output of grind's msz-mqapply + if errcatch(all_ops_idp(_v)) = [] + then _v:STACKERROR, + */ %_tmp:[[_k, stack_dispvalue(_v)]], if listp(%_tmp) then _DVALUES:append(_DVALUES,%_tmp), 0)$ diff --git a/stack/maxima/stacktex.lisp b/stack/maxima/stacktex.lisp index d9b7bfeeb4..07ed4f21f4 100644 --- a/stack/maxima/stacktex.lisp +++ b/stack/maxima/stacktex.lisp @@ -530,3 +530,22 @@ x (tex-list (cdr x) nil r (or (nth 2 (texsym (caar x))) (if (string= $stackfltsep '",") '" ; " '" , ")))) (append l x)) + +;; ************************************************************************************************* +;; Added 17 Oct 2024. +;; +;; Change the output of mqapply, so that we explicitly print the "apply" function name +;; E.g. (X+1)(x,y,z); is valid Maxima. The STACK parser (in PHP) needs apply((X+1),[x,y,z]); +;; + +;; Original code from grind.lisp +;(defun msz-mqapply (x l r) +; (setq l (msize (cadr x) l (list #\( ) lop 'mfunction) +; r (msize-list (cddr x) nil (cons #\) r))) +; (cons (+ (car l) (car r)) (cons l (cdr r)))) + +(defun msz-mqapply (x l r) + (setq l (msize (cadr x) (append '(#\( #\y #\l #\p #\p #\a ) l) '(#\, #\[ ) lop 'mfunction) + r (msize-list (cddr x) nil (append '(#\] #\)) r))) + (cons (+ (car l) (car r)) (cons l (cdr r)))) + diff --git a/stack/maxima/utils.mac b/stack/maxima/utils.mac index e471af0d9c..bcec78135a 100644 --- a/stack/maxima/utils.mac +++ b/stack/maxima/utils.mac @@ -209,8 +209,6 @@ sig_figs_from_str(strexp) := block([leadingzeros,indefinitezeros,trailingzeros,m return(r) )$ - - FORBIDDEN_SYMBOLS_SET: {"%th", "adapth_depth", "alias", "aliases", "alphabetic", "appendfile", "apropos", "assume_external_byte_order", "backtrace", "batch", "barsplot", "batchload", "boxchar", "boxplot", "bug_report", "build_info", "catch", "chdir", "close", "closefile", @@ -263,6 +261,10 @@ all_ops(%_expr) := block([%_edge, %_next_edge, %_tmp, %_op, %_result], %_edge : %_next_edge, %_next_edge : [], for %_tmp in %_edge do ( + /* Ensure all operators are identifiers. */ + if (not(OPT_APPLY_COMPOUND) and not(atom(%_tmp))) then + if not(atom(op(%_tmp))) then + error(sconcat("STACK does not support non-atomic identifiers. Attempt to apply a non-atomic identifier detected: ", safe_op(%_tmp))), %_op : safe_op(%_tmp), if not (%_op = "") then ( %_result : append(%_result, [%_op]), @@ -273,6 +275,27 @@ all_ops(%_expr) := block([%_edge, %_next_edge, %_tmp, %_op, %_result], %_result )$ +/* This checks if all operators in a generated expression are identifiers. */ +all_ops_idp(%_expr) := block([%_edge, %_next_edge, %_tmp, %_op], + /* Returns a list of all the operators and functions + in use in the expression. Turn it to a bag if you need + the counts or a set if only the existence matters. */ + %_next_edge : [%_expr], + while length(%_next_edge) > 0 do ( + %_edge : %_next_edge, + %_next_edge : [], + for %_tmp in %_edge do ( + /* Ensure all operators are identifiers. */ + if (not(OPT_APPLY_COMPOUND) and not(atom(%_tmp))) then ( + if not(atom(op(%_tmp))) then + error(sconcat("STACK does not support non-atomic identifiers. Attempt to apply a non-atomic identifier detected: ", safe_op(%_tmp))), + %_next_edge : append(%_next_edge, args(%_tmp)) + ) + ) + ), + true +)$ + %_C(%_id) := block([simp], simp:true, if elementp(sconcat(%_id), FORBIDDEN_SYMBOLS_SET) then ( error(sconcat("Attempt to call forbidden function detected: ", %_id)) diff --git a/tests/caskeyval_exception_test.php b/tests/caskeyval_exception_test.php index fc12f609d9..10500f61f4 100644 --- a/tests/caskeyval_exception_test.php +++ b/tests/caskeyval_exception_test.php @@ -65,18 +65,4 @@ public function test_exception_7() { $this->expectException(stack_exception::class); $at1 = new stack_cas_keyval('x=1', 't', false); } - - public function test_stack_compile_unexpected_lambda() { - $this->expectException(stack_exception::class); - // This is related to issue #1279. - $tests = 'a:b+1; c:a-a(d+1);'; - $kv = new stack_cas_keyval($tests); - $this->asserttrue($kv->get_valid()); - $expected = []; - $this->assertEquals($expected, $kv->get_errors()); - $kv->instantiate(); - $s = $kv->get_session(); - $s->instantiate(); - $this->assertEquals($s->get_by_key('c')->get_evaluationform(), 'c:(b+1)-(b+1)(d+1)'); - } } diff --git a/tests/caskeyval_test.php b/tests/caskeyval_test.php index 1ab8874b92..e50fa24010 100644 --- a/tests/caskeyval_test.php +++ b/tests/caskeyval_test.php @@ -376,5 +376,43 @@ public function test_stack_compile_unexpected_lambda() { 'c:(b+1)-(b+1)' . '*(d+1).']; $this->assertEquals($expected, $kv->get_errors()); + + $tests = 'a:b+1; c:a-a(d+1);'; + $kv = new stack_cas_keyval($tests); + $this->asserttrue($kv->get_valid()); + $expected = []; + $this->assertEquals($expected, $kv->get_errors()); + $kv->instantiate(); + $s = $kv->get_session(); + $expected = "STACK does not support non-atomic identifiers. " . + "Attempt to apply a non-atomic identifier detected: b+1"; + $this->assertEquals($expected, $s->get_errors()); + + $tests = 'OPT_APPLY_COMPOUND:true; a:b+1; c:a-a(d+1);'; + $kv = new stack_cas_keyval($tests); + $this->asserttrue($kv->get_valid()); + $expected = []; + $this->assertEquals($expected, $kv->get_errors()); + $kv->instantiate(); + $s = $kv->get_session(); + $expected = ""; + $this->assertEquals($expected, $s->get_errors()); + $expected = "OPT_APPLY_COMPOUND:true;\n" . + "a:b+1;\n" . + "c:a-a(d+1);"; + $this->assertEquals($expected, $s->get_keyval_representation()); + $cs = $s->get_by_key('c'); + $this->assertEquals('-apply((b+1),[d+1])+b+1', $cs->get_value()); + + $tests = 'a:[A+2,B,[1,2,3],D,E]; c:apply(a[1],a[3]);'; + $kv = new stack_cas_keyval($tests); + $this->asserttrue($kv->get_valid()); + $expected = []; + $this->assertEquals($expected, $kv->get_errors()); + $kv->instantiate(); + $s = $kv->get_session(); + $expected = "STACK does not support non-atomic identifiers. " . + "Attempt to apply a non-atomic identifier detected: A+2"; + $this->assertEquals($expected, $s->get_errors()); } } diff --git a/tests/castext_test.php b/tests/castext_test.php index 66f49baec8..732049fe8d 100644 --- a/tests/castext_test.php +++ b/tests/castext_test.php @@ -2457,7 +2457,7 @@ public function test_make_mult_sgn_stackunits() { * @covers \qtype_stack\stack_cas_keyval */ public function test_unexpected_lambda() { - $a2 = ['a:b+1', 'c:a-a(d+1)']; + $a2 = ['OPT_APPLY_COMPOUND:true', 'a:b+1', 'c:a-a(d+1)']; $s2 = []; foreach ($a2 as $s) { $cs = stack_ast_container::make_from_teacher_source($s, '', new stack_cas_security(), []); @@ -2474,5 +2474,24 @@ public function test_unexpected_lambda() { $cs2->instantiate(); $expected = '\({b+1-\left(b+1\right)(d+1)}\)'; $this->assertEquals($expected, $at1->get_rendered()); + + $a2 = ['a:b+1', 'c:a-a(d+1)']; + $s2 = []; + foreach ($a2 as $s) { + $cs = stack_ast_container::make_from_teacher_source($s, '', new stack_cas_security(), []); + $this->assertTrue($cs->get_valid()); + $s2[] = $cs; + } + $options = new stack_options(); + $options->set_option('simplify', false); + $cs2 = new stack_cas_session2($s2, $options, 0); + $at1 = castext2_evaluatable::make_from_source('{@c@}', + 'test-case'); + $this->assertTrue($at1->get_valid()); + $cs2->add_statement($at1); + $cs2->instantiate(); + $expected = '

Rendering of text content failed.

'; + $this->assertEquals($expected, $at1->get_rendered()); } }