Skip to content

Commit 053566e

Browse files
committed
cope with implicit multiplication inside or after anonymous functions
1 parent 388872c commit 053566e

4 files changed

Lines changed: 107 additions & 51 deletions

File tree

‎runtime/scripts/jme.js‎

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,7 +1687,7 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
16871687
var name = this.funcSynonym(tok.nameWithoutAnnotation);
16881688
var ntok = new TFunc(name,tok.annotation);
16891689
ntok.pos = tok.pos;
1690-
this.stack.push(ntok);
1690+
this.addstack(ntok);
16911691
} else {
16921692
//this is a variable otherwise
16931693
this.addoutput(tok);
@@ -1699,16 +1699,16 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
16991699
}
17001700
//reached end of expression defining function parameter, so pop all of its operations off stack and onto output
17011701
while( this.stack.length > 0 && this.stack.at(-1).type != "(" && this.stack.at(-1).type != '[') {
1702-
this.addoutput(this.stack.pop())
1702+
this.addoutput(this.popstack())
17031703
}
17041704
this.numvars[this.numvars.length-1]++;
17051705
if( ! this.stack.length ) {
17061706
throw(new Numbas.Error('jme.shunt.no left bracket in function'));
17071707
}
17081708
},
17091709
'op': function(tok) {
1710-
if(tok.name == '*' && this.output.at(-1)?.tok.type=='lambda') {
1711-
this.stack.push(this.output.pop().tok);
1710+
if(tok.name == '*' && this.output.at(-1)?.tok.type=='lambda' && !this.output.at(-1)?.args) {
1711+
this.addstack(this.popoutput().tok);
17121712
this.numvars.push(0);
17131713
return;
17141714
}
@@ -1735,10 +1735,10 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
17351735
return false;
17361736
}
17371737
while(should_pop.apply(this)) {
1738-
this.addoutput(this.stack.pop());
1738+
this.addoutput(this.popstack());
17391739
}
17401740
}
1741-
this.stack.push(tok);
1741+
this.addstack(tok);
17421742
},
17431743
'[': function(tok) {
17441744
var i = this.i;
@@ -1750,20 +1750,20 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
17501750
else {
17511751
this.listmode.push('index');
17521752
}
1753-
this.stack.push(tok);
1753+
this.addstack(tok);
17541754
this.numvars.push(0);
17551755
},
17561756
']': function(tok) {
17571757
while( this.stack.length > 0 && this.stack.at(-1).type != "[" ) {
1758-
this.addoutput(this.stack.pop());
1758+
this.addoutput(this.popstack());
17591759
}
17601760
if(this.tokens[this.i-1].type != ',' && this.tokens[this.i-1].type != '[') {
17611761
this.numvars[this.numvars.length-1] += 1;
17621762
}
17631763
if( ! this.stack.length ) {
17641764
throw(new Numbas.Error('jme.shunt.no left square bracket'));
17651765
} else {
1766-
this.stack.pop(); //get rid of left bracket
1766+
this.popstack(); //get rid of left bracket
17671767
}
17681768
//work out size of list
17691769
var n = this.numvars.pop();
@@ -1782,17 +1782,17 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
17821782
}
17831783
},
17841784
'(': function(tok) {
1785-
this.stack.push(tok);
1785+
this.addstack(tok);
17861786
this.numvars.push(0);
17871787
},
17881788
')': function(tok) {
17891789
while( this.stack.length > 0 && this.stack.at(-1)?.type != "(" ) {
1790-
this.addoutput(this.stack.pop());
1790+
this.addoutput(this.popstack());
17911791
}
17921792
if( ! this.stack.length ) {
17931793
throw(new Numbas.Error('jme.shunt.no left bracket'));
17941794
}
1795-
this.stack.pop(); //get rid of left bracket
1795+
this.popstack(); //get rid of left bracket
17961796

17971797
//work out number of items between the brackets
17981798
var n = this.numvars.pop();
@@ -1801,8 +1801,8 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
18011801
}
18021802

18031803
//if this is a function call, then the next thing on the stack should be a function name, which we need to pop
1804-
if(this.stack.length > 0 && this.stack.at(-1)?.type=="function" || (this.stack.at(-1)?.type=="lambda" && this.stack.at(-1)?.names !== undefined)) {
1805-
var f = this.stack.pop();
1804+
if(this.stack.length > 0 && this.stack.at(-1)?.type=="function" || (this.stack.at(-1)?.type=="lambda" && this.stack.at(-1)?.names !== undefined && (this.i==this.tokens.length-1 || !jme.isOp(this.tokens[this.i+1], '*')))) {
1805+
var f = this.popstack();
18061806
f.vars = n;
18071807
this.addoutput(f);
18081808
//if this is the list of argument names for an anonymous function, add them to the lambda token, which is next.
@@ -1832,10 +1832,10 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
18321832
throw(new Numbas.Error('jme.shunt.keypair in wrong place'));
18331833
}
18341834
tok.pairmode = pairmode;
1835-
this.stack.push(tok);
1835+
this.addstack(tok);
18361836
},
18371837
'lambda': function(tok) {
1838-
this.stack.push(tok);
1838+
this.addstack(tok);
18391839
}
18401840
},
18411841

@@ -1952,13 +1952,31 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
19521952
};
19531953
}
19541954
}
1955+
1956+
if(thing.tok.type=='lambda') {
1957+
thing.tok.vars = thing.tok.names.length;
1958+
}
19551959
this.output.push(thing);
19561960
}
19571961
else {
19581962
this.output.push({tok:tok});
19591963
}
19601964
},
19611965

1966+
popoutput: function() {
1967+
const tree = this.output.pop();
1968+
return tree;
1969+
},
1970+
1971+
addstack: function(tok) {
1972+
this.stack.push(tok);
1973+
},
1974+
1975+
popstack: function() {
1976+
const tok = this.stack.pop();
1977+
return tok;
1978+
},
1979+
19621980
/** Shunt list of tokens into a syntax tree. Uses the shunting yard algorithm.
19631981
*
19641982
* @param {Array.<Numbas.jme.token>} tokens
@@ -2002,7 +2020,7 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
20022020
type_actions[')'].apply(this);
20032021
}
20042022
} else {
2005-
this.stack.pop();
2023+
this.popstack();
20062024
this.addoutput(x);
20072025
}
20082026
}

‎tests/jme-runtime.js‎

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10461,7 +10461,7 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1046110461
var name = this.funcSynonym(tok.nameWithoutAnnotation);
1046210462
var ntok = new TFunc(name,tok.annotation);
1046310463
ntok.pos = tok.pos;
10464-
this.stack.push(ntok);
10464+
this.addstack(ntok);
1046510465
} else {
1046610466
//this is a variable otherwise
1046710467
this.addoutput(tok);
@@ -10473,16 +10473,16 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1047310473
}
1047410474
//reached end of expression defining function parameter, so pop all of its operations off stack and onto output
1047510475
while( this.stack.length > 0 && this.stack.at(-1).type != "(" && this.stack.at(-1).type != '[') {
10476-
this.addoutput(this.stack.pop())
10476+
this.addoutput(this.popstack())
1047710477
}
1047810478
this.numvars[this.numvars.length-1]++;
1047910479
if( ! this.stack.length ) {
1048010480
throw(new Numbas.Error('jme.shunt.no left bracket in function'));
1048110481
}
1048210482
},
1048310483
'op': function(tok) {
10484-
if(tok.name == '*' && this.output.at(-1)?.tok.type=='lambda') {
10485-
this.stack.push(this.output.pop().tok);
10484+
if(tok.name == '*' && this.output.at(-1)?.tok.type=='lambda' && !this.output.at(-1)?.args) {
10485+
this.addstack(this.popoutput().tok);
1048610486
this.numvars.push(0);
1048710487
return;
1048810488
}
@@ -10509,10 +10509,10 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1050910509
return false;
1051010510
}
1051110511
while(should_pop.apply(this)) {
10512-
this.addoutput(this.stack.pop());
10512+
this.addoutput(this.popstack());
1051310513
}
1051410514
}
10515-
this.stack.push(tok);
10515+
this.addstack(tok);
1051610516
},
1051710517
'[': function(tok) {
1051810518
var i = this.i;
@@ -10524,20 +10524,20 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1052410524
else {
1052510525
this.listmode.push('index');
1052610526
}
10527-
this.stack.push(tok);
10527+
this.addstack(tok);
1052810528
this.numvars.push(0);
1052910529
},
1053010530
']': function(tok) {
1053110531
while( this.stack.length > 0 && this.stack.at(-1).type != "[" ) {
10532-
this.addoutput(this.stack.pop());
10532+
this.addoutput(this.popstack());
1053310533
}
1053410534
if(this.tokens[this.i-1].type != ',' && this.tokens[this.i-1].type != '[') {
1053510535
this.numvars[this.numvars.length-1] += 1;
1053610536
}
1053710537
if( ! this.stack.length ) {
1053810538
throw(new Numbas.Error('jme.shunt.no left square bracket'));
1053910539
} else {
10540-
this.stack.pop(); //get rid of left bracket
10540+
this.popstack(); //get rid of left bracket
1054110541
}
1054210542
//work out size of list
1054310543
var n = this.numvars.pop();
@@ -10556,17 +10556,17 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1055610556
}
1055710557
},
1055810558
'(': function(tok) {
10559-
this.stack.push(tok);
10559+
this.addstack(tok);
1056010560
this.numvars.push(0);
1056110561
},
1056210562
')': function(tok) {
1056310563
while( this.stack.length > 0 && this.stack.at(-1)?.type != "(" ) {
10564-
this.addoutput(this.stack.pop());
10564+
this.addoutput(this.popstack());
1056510565
}
1056610566
if( ! this.stack.length ) {
1056710567
throw(new Numbas.Error('jme.shunt.no left bracket'));
1056810568
}
10569-
this.stack.pop(); //get rid of left bracket
10569+
this.popstack(); //get rid of left bracket
1057010570

1057110571
//work out number of items between the brackets
1057210572
var n = this.numvars.pop();
@@ -10575,8 +10575,8 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1057510575
}
1057610576

1057710577
//if this is a function call, then the next thing on the stack should be a function name, which we need to pop
10578-
if(this.stack.length > 0 && this.stack.at(-1)?.type=="function" || (this.stack.at(-1)?.type=="lambda" && this.stack.at(-1)?.names !== undefined)) {
10579-
var f = this.stack.pop();
10578+
if(this.stack.length > 0 && this.stack.at(-1)?.type=="function" || (this.stack.at(-1)?.type=="lambda" && this.stack.at(-1)?.names !== undefined && (this.i==this.tokens.length-1 || !jme.isOp(this.tokens[this.i+1], '*')))) {
10579+
var f = this.popstack();
1058010580
f.vars = n;
1058110581
this.addoutput(f);
1058210582
//if this is the list of argument names for an anonymous function, add them to the lambda token, which is next.
@@ -10606,10 +10606,10 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1060610606
throw(new Numbas.Error('jme.shunt.keypair in wrong place'));
1060710607
}
1060810608
tok.pairmode = pairmode;
10609-
this.stack.push(tok);
10609+
this.addstack(tok);
1061010610
},
1061110611
'lambda': function(tok) {
10612-
this.stack.push(tok);
10612+
this.addstack(tok);
1061310613
}
1061410614
},
1061510615

@@ -10726,13 +10726,31 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1072610726
};
1072710727
}
1072810728
}
10729+
10730+
if(thing.tok.type=='lambda') {
10731+
thing.tok.vars = thing.tok.names.length;
10732+
}
1072910733
this.output.push(thing);
1073010734
}
1073110735
else {
1073210736
this.output.push({tok:tok});
1073310737
}
1073410738
},
1073510739

10740+
popoutput: function() {
10741+
const tree = this.output.pop();
10742+
return tree;
10743+
},
10744+
10745+
addstack: function(tok) {
10746+
this.stack.push(tok);
10747+
},
10748+
10749+
popstack: function() {
10750+
const tok = this.stack.pop();
10751+
return tok;
10752+
},
10753+
1073610754
/** Shunt list of tokens into a syntax tree. Uses the shunting yard algorithm.
1073710755
*
1073810756
* @param {Array.<Numbas.jme.token>} tokens
@@ -10776,7 +10794,7 @@ jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
1077610794
type_actions[')'].apply(this);
1077710795
}
1077810796
} else {
10779-
this.stack.pop();
10797+
this.popstack();
1078010798
this.addoutput(x);
1078110799
}
1078210800
}

‎tests/jme/jme-tests.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,8 @@ Numbas.queueScript('jme_tests',['qunit','jme','jme-rules','jme-display','jme-cal
796796
['let(f, n -> 2i for: i of: 1..n, map(f(j),j,1..5))', [[2],[2,4],[2,4,6],[2,4,6,8],[2,4,6,8,10]], 'variables are kept free inside list comprehensions'],
797797
['(x -> -x)(1)', -1, 'unary operator at the front of the function body'],
798798
['(x -> (-x))(1)', -1, 'unary operator at the front of the bracketed function body'],
799+
['((a,b) -> (a+b)(a-b))(5,3)', 16, 'implicit multiplication in function body'],
800+
['(a -> a^2)(2)(3)', 36, 'implicit multiplication after lambda application'],
799801
];
800802
tests.forEach(([expr, value, note]) => {
801803
deepCloseEqual(assert, jme.unwrapValue(evaluate(expr)), value, expr+(note ? (' - '+note) : ''));

0 commit comments

Comments
 (0)