Skip to content

Commit 165e20d

Browse files
committed
LDEV-6428 reduce member function runtime execution overhead
1 parent 84789f8 commit 165e20d

6 files changed

Lines changed: 288 additions & 19 deletions

File tree

core/src/main/java/lucee/runtime/interpreter/ref/cast/Casting.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
**/
1919
package lucee.runtime.interpreter.ref.cast;
2020

21+
import lucee.commons.lang.CFTypes;
2122
import lucee.runtime.PageContext;
2223
import lucee.runtime.exp.PageException;
2324
import lucee.runtime.interpreter.ref.Ref;
@@ -33,12 +34,12 @@ public final class Casting extends RefSupport implements Ref {
3334

3435
private final short type;
3536
private final String strType;
36-
private Ref ref;
37-
private Object val;
37+
private final Ref ref;
38+
private final Object val;
3839

3940
/**
4041
* constructor of the class
41-
*
42+
*
4243
* @param strType
4344
* @param type
4445
* @param ref
@@ -47,25 +48,28 @@ public Casting(String strType, short type, Ref ref) {
4748
this.type = type;
4849
this.strType = strType;
4950
this.ref = ref;
51+
this.val = null;
5052
}
5153

5254
public Casting(String strType, short type, Object val) {
5355
this.type = type;
5456
this.strType = strType;
57+
this.ref = null;
5558
this.val = val;
5659
}
5760

5861
public Casting(FunctionLibFunctionArg flfa, Object val) {
5962
this.type = flfa.getType();
6063
this.strType = flfa.getTypeAsString();
64+
this.ref = null;
6165
this.val = val;
6266
}
6367

6468
@Override
6569
public Object getValue(PageContext pc) throws PageException {
6670
// if ref == null, it is val based Casting
6771
if (ref == null) return Caster.castTo(pc, type, strType, val);
68-
if (ref instanceof Variable && "queryColumn".equalsIgnoreCase(strType)) {
72+
if (type == CFTypes.TYPE_QUERY_COLUMN && ref instanceof Variable) {
6973
Variable var = (Variable) ref;
7074
return Caster.castTo(pc, type, strType, var.getCollection(pc));
7175
}

core/src/main/java/lucee/runtime/interpreter/ref/func/BIFCall.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public Object getValue(PageContext pc) throws PageException {
110110
flfa = it.next();
111111
vt = getMatchingValueAndType(flfa, fvalues, names);
112112
if (vt.index != -1) names[vt.index] = null;
113-
arguments[index++] = new Casting(vt.type, CFTypes.toShort(vt.type, false, CFTypes.TYPE_UNKNOW), vt.value).getValue(pc);
113+
arguments[index++] = Caster.castTo(pc, CFTypes.toShort(vt.type, false, CFTypes.TYPE_UNKNOW), vt.type, vt.value);
114114
}
115115

116116
for (int y = 0; y < names.length; y++) {

core/src/main/java/lucee/runtime/type/util/MemberUtil.java

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@
3232
import lucee.commons.lang.StringUtil;
3333
import lucee.runtime.PageContext;
3434
import lucee.runtime.config.ConfigWebPro;
35+
import lucee.runtime.exp.CasterException;
3536
import lucee.runtime.exp.ExpressionException;
3637
import lucee.runtime.exp.FunctionException;
3738
import lucee.runtime.exp.PageException;
39+
import lucee.runtime.ext.function.BIF;
3840
import lucee.runtime.interpreter.ref.Ref;
3941
import lucee.runtime.interpreter.ref.cast.Casting;
4042
import lucee.runtime.interpreter.ref.func.BIFCall;
@@ -137,23 +139,57 @@ else if (type == CFTypes.TYPE_STRING) {
137139
if (member != null) {
138140
List<FunctionLibFunctionArg> _args = member.getArg();
139141
if (args.length < _args.size()) {
140-
ArrayList<Ref> refs = new ArrayList<Ref>();
141-
142+
// LDEV-6428: build Object[] directly, skip Casting/ArrayList/Ref[]/BIFCall ceremony.
143+
// All member-callable BIFs do their own Caster.to* coercion in invoke() (audited
144+
// 2026-06-25: 284 of 284 member-callable BIFs use defensive coercion or instanceof-
145+
// guarded casts), so the upstream Caster.castTo wrapper is double-work.
146+
int n = _args.size();
147+
Object[] callArgs = new Object[n];
148+
int filled = 0;
142149
int pos = member.getMemberPosition();
143-
FunctionLibFunctionArg flfa;
144-
Iterator<FunctionLibFunctionArg> it = _args.iterator();
145-
int glbIndex = 0, argIndex = -1;
146-
while (it.hasNext()) {
147-
glbIndex++;
148-
flfa = it.next();
150+
int argIndex = -1;
151+
for (int glbIndex = 1; glbIndex <= n; glbIndex++) {
149152
if (glbIndex == pos) {
150-
refs.add(new Casting(strType, type, coll));
153+
callArgs[filled++] = coll;
154+
}
155+
else if (args.length > ++argIndex) {
156+
callArgs[filled++] = args[argIndex];
157+
}
158+
}
159+
if (filled < callArgs.length) {
160+
Object[] trimmed = new Object[filled];
161+
System.arraycopy(callArgs, 0, trimmed, 0, filled);
162+
callArgs = trimmed;
163+
}
164+
BIF bif = member.getBIF();
165+
// Preserve BIFCall.getValue check order: memberChaining short-circuit BEFORE
166+
// argMin (matches pre-fastpath behavior where memberChaining BIFs invoke even
167+
// with too few args, then throw from inside via their own validation, producing
168+
// the BIF's hardcoded camelCase function name in the error message).
169+
if (member.getMemberChaining()) {
170+
try {
171+
bif.invoke(pc, callArgs);
151172
}
152-
else if (args.length > ++argIndex) { // careful, argIndex is only incremented when condition above is false
153-
refs.add(new Casting(flfa.getTypeAsString(), flfa.getType(), args[argIndex]));
173+
catch (CasterException ce) {
174+
rethrowWithFLDType(pc, _args, callArgs, ce);
154175
}
176+
return coll;
177+
}
178+
// argMin enforcement uses getNameWithCase() to match pre-fastpath BIFCall behavior
179+
// where the FLD-declared case is preserved in the error message.
180+
if (member.getArgType() != FunctionLibFunction.ARG_DYNAMIC && member.getArgMin() > callArgs.length) {
181+
throw new FunctionException(pc, member.getNameWithCase(), member.getArgMin(), _args.size(), callArgs.length);
155182
}
156-
return new BIFCall(coll, member, refs.toArray(new Ref[refs.size()])).getValue(pc);
183+
// Preserve BIFCall.getValue return-type cast.
184+
Object rawResult;
185+
try {
186+
rawResult = bif.invoke(pc, callArgs);
187+
}
188+
catch (CasterException ce) {
189+
rethrowWithFLDType(pc, _args, callArgs, ce);
190+
return null; // unreachable -- rethrowWithFLDType always throws
191+
}
192+
return Caster.castTo(pc, member.getReturnTypeAsString(), rawResult, false);
157193
}
158194
else throw new FunctionException(pc, member.getName(), member.getArgMin(), _args.size(), args.length);
159195
}
@@ -204,6 +240,23 @@ private static Object callMethod(Object obj, Collection.Key methodName, Object[]
204240
}
205241
}
206242

243+
// LDEV-6428: on a CasterException from inside a member-dispatched BIF, retry each
244+
// arg against its FLD-declared type to surface a user-friendly error referencing the
245+
// FLD-declared type ("string", "numeric", "queryColumn") instead of the BIF's internal
246+
// Java-typed cast target (e.g. "lucee.runtime.type.Collection$Key").
247+
// Only invoked on the slow path (after the BIF has already thrown).
248+
private static void rethrowWithFLDType(PageContext pc, List<FunctionLibFunctionArg> _args, Object[] callArgs, CasterException original) throws PageException {
249+
int n = Math.min(callArgs.length, _args.size());
250+
for (int i = 0; i < n; i++) {
251+
FunctionLibFunctionArg flfa = _args.get(i);
252+
Caster.castTo(pc, flfa.getType(), flfa.getTypeAsString(), callArgs[i]);
253+
// if the cast succeeded, this arg is not the culprit -- continue
254+
}
255+
// All args cast cleanly against FLD-declared types -- the BIF's exception was not
256+
// an arg-type problem (internal logic error, etc). Propagate the original.
257+
throw original;
258+
}
259+
207260
// used in extension image
208261
public static Object callWithNamedValues(PageContext pc, Object coll, Collection.Key methodName, Struct args, short type, String strType) throws PageException {
209262
Map<Key, FunctionLibFunction> members = getMembers(pc, type);

loader/build.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<project default="core" basedir="." name="Lucee"
33
xmlns:resolver="antlib:org.apache.maven.resolver.ant">
44

5-
<property name="version" value="8.0.0.143-SNAPSHOT"/>
5+
<property name="version" value="8.0.0.144-SNAPSHOT"/>
66

77
<taskdef uri="antlib:org.apache.maven.resolver.ant" resource="org/apache/maven/resolver/ant/antlib.xml">
88
<classpath>

loader/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>org.lucee</groupId>
55
<artifactId>lucee</artifactId>
6-
<version>8.0.0.143-SNAPSHOT</version>
6+
<version>8.0.0.144-SNAPSHOT</version>
77
<packaging>jar</packaging>
88

99
<name>Lucee Loader Build</name>

test/tickets/LDEV6428.cfc

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
component extends="org.lucee.cfml.test.LuceeTestCase" {
2+
3+
function run( testResults, testBox ) {
4+
describe( "LDEV-6428 Reduce member function runtime execution overhead", function() {
5+
6+
// These tests pin the behaviour of `Casting.getValue` on the queryColumn
7+
// special-case branch -- only reached via the CFML expression interpreter
8+
// path (evaluate, dynamic dispatch), NOT compiled bytecode. Compiled paths
9+
// for valueList/valueArray/quotedValueList go through CastOther, a
10+
// different code path. The interpreter path through Casting.java:68 was
11+
// previously untested.
12+
13+
describe( "queryColumn cast via interpreted expression (Casting.getValue)", function() {
14+
15+
it( "evaluate( 'valueList(q.col)' ) resolves to delimited column values", function() {
16+
var q = queryNew( "a", "varchar", [ [ "x" ], [ "y" ], [ "z" ] ] );
17+
expect( evaluate( "valueList(q.a)" ) ).toBe( "x,y,z" );
18+
});
19+
20+
it( "evaluate( 'valueList(q.col, delim)' ) honours custom delimiter", function() {
21+
var q = queryNew( "a", "varchar", [ [ "1" ], [ "2" ], [ "3" ] ] );
22+
expect( evaluate( "valueList(q.a, ';')" ) ).toBe( "1;2;3" );
23+
});
24+
25+
it( "evaluate( 'valueArray(q.col)' ) returns array of column values", function() {
26+
var q = queryNew( "a", "integer", [ [ 1 ], [ 2 ], [ 3 ] ] );
27+
var result = evaluate( "valueArray(q.a)" );
28+
expect( arrayLen( result ) ).toBe( 3 );
29+
expect( result[ 1 ] ).toBe( 1 );
30+
expect( result[ 2 ] ).toBe( 2 );
31+
expect( result[ 3 ] ).toBe( 3 );
32+
});
33+
34+
it( "evaluate( 'quotedValueList(q.col)' ) returns quoted delimited values", function() {
35+
var q = queryNew( "a", "varchar", [ [ "foo" ], [ "bar" ] ] );
36+
expect( evaluate( "quotedValueList(q.a)" ) ).toBe( "'foo','bar'" );
37+
});
38+
39+
it( "queryColumn cast handles single-row query", function() {
40+
var q = queryNew( "a", "varchar", [ [ "only" ] ] );
41+
expect( evaluate( "valueList(q.a)" ) ).toBe( "only" );
42+
});
43+
44+
it( "queryColumn cast handles empty query", function() {
45+
var q = queryNew( "a", "varchar" );
46+
expect( evaluate( "valueList(q.a)" ) ).toBe( "" );
47+
expect( arrayLen( evaluate( "valueArray(q.a)" ) ) ).toBe( 0 );
48+
});
49+
});
50+
51+
describe( "queryColumn cast via compiled bytecode (CastOther) -- regression guard", function() {
52+
53+
it( "valueList(q.col) inline resolves correctly", function() {
54+
var q = queryNew( "a", "varchar", [ [ "x" ], [ "y" ], [ "z" ] ] );
55+
expect( valueList( q.a ) ).toBe( "x,y,z" );
56+
});
57+
58+
it( "valueArray(q.col) inline resolves correctly", function() {
59+
var q = queryNew( "a", "integer", [ [ 1 ], [ 2 ], [ 3 ] ] );
60+
var result = valueArray( q.a );
61+
expect( arrayLen( result ) ).toBe( 3 );
62+
});
63+
64+
it( "quotedValueList(q.col) inline resolves correctly", function() {
65+
var q = queryNew( "a", "varchar", [ [ "foo" ], [ "bar" ] ] );
66+
expect( quotedValueList( q.a ) ).toBe( "'foo','bar'" );
67+
});
68+
});
69+
70+
describe( "member-method dispatch -- MemberUtil.call -> BIFCall -> Casting.getValue", function() {
71+
72+
it( "qry.valueList(column) member call resolves", function() {
73+
var q = queryNew( "a", "varchar", [ [ "x" ], [ "y" ], [ "z" ] ] );
74+
expect( q.valueList( "a" ) ).toBe( "x,y,z" );
75+
});
76+
77+
it( "struct member call (struct.keyArray) dispatches through MemberUtil", function() {
78+
var s = { foo=1, bar=2 };
79+
var keys = s.keyArray();
80+
expect( arrayLen( keys ) ).toBe( 2 );
81+
});
82+
83+
it( "array member call (array.len) dispatches through MemberUtil", function() {
84+
var a = [ 10, 20, 30 ];
85+
expect( a.len() ).toBe( 3 );
86+
});
87+
88+
it( "member call with named args dispatches through BIFCall named-arg path (line 113)", function() {
89+
var s = { foo=1, bar=2 };
90+
// structKeyExists named-arg via member
91+
expect( s.keyExists( key="foo" ) ).toBeTrue();
92+
expect( s.keyExists( key="missing" ) ).toBeFalse();
93+
});
94+
});
95+
96+
describe( "evaluate() named-arg dispatch -- BIFCall.getValue:113 path", function() {
97+
98+
it( "evaluate with named args resolves through Casting wrapper in BIFCall", function() {
99+
expect( evaluate( "structKeyExists(struct={a:1,b:2}, key='a')" ) ).toBeTrue();
100+
expect( evaluate( "structKeyExists(struct={a:1,b:2}, key='zz')" ) ).toBeFalse();
101+
});
102+
});
103+
104+
describe( "exception messages must match pre-LDEV-6428 .143 baseline", function() {
105+
106+
// These tests lock in the exact exception messages produced on .143 (the
107+
// pre-LDEV-6428 baseline). Captured from D:/tmp/ldev6428-throws/run-143.log
108+
// on 2026-06-25. Any deviation indicates a behavioural change in error
109+
// reporting that downstream users may grep against.
110+
111+
it( "wrong arg type (replace with struct) keeps the 'second Argument [sub1] is invalid' message", function() {
112+
try {
113+
"zac,claude,joshi".replace( { x = 1 }, "+" );
114+
fail( "expected exception" );
115+
}
116+
catch ( any e ) {
117+
expect( e.type ).toBe( "expression" );
118+
expect( e.message ).toBe( "Invalid call of the function [replace], second Argument [sub1] is invalid, When passing three parameters or more, the second parameter must be a simple value." );
119+
}
120+
});
121+
122+
it( "missing required arg (arr.append()) keeps the 'too few arguments for function [ArrayAppend] call' message", function() {
123+
try {
124+
var arr = [ 1, 2, 3 ];
125+
arr.append();
126+
fail( "expected exception" );
127+
}
128+
catch ( any e ) {
129+
expect( e.type ).toBe( "expression" );
130+
expect( e.message ).toBe( "too few arguments for function [ArrayAppend] call" );
131+
}
132+
});
133+
134+
it( "too many args (arr.len(...extras...)) keeps the 'too many arguments for function [arraylen] call' message", function() {
135+
try {
136+
var arr = [ 1, 2, 3 ];
137+
arr.len( "extra1", "extra2", "extra3", "extra4" );
138+
fail( "expected exception" );
139+
}
140+
catch ( any e ) {
141+
expect( e.type ).toBe( "expression" );
142+
expect( e.message ).toBe( "too many arguments for function [arraylen] call" );
143+
}
144+
});
145+
146+
it( "receiver type mismatch (numeric.keyArray) keeps the 'function [keyArray] does not exist in the Numeric' message shape", function() {
147+
// The "Available functions are [...]" list is runtime-derived from the
148+
// FLD member-function registry plus any-typed BIFs. Installed extensions
149+
// can add or remove entries from this list, so we lock the message shape
150+
// (prefix + bracketed list + ending) rather than the exact contents.
151+
try {
152+
var n = 123;
153+
n.keyArray();
154+
fail( "expected exception" );
155+
}
156+
catch ( any e ) {
157+
expect( e.type ).toBe( "expression" );
158+
expect( e.message ).toMatch( "^The function \[keyArray\] does not exist in the Numeric\. Available functions are \[.+\]\.$" );
159+
}
160+
});
161+
162+
it( "invalid sort type keeps the 'invalid sort type [...]' message", function() {
163+
try {
164+
var arr = [ 3, 1, 2 ];
165+
arr.sort( "not_a_valid_sort_type" );
166+
fail( "expected exception" );
167+
}
168+
catch ( any e ) {
169+
expect( e.type ).toBe( "expression" );
170+
expect( e.message ).toBe( "invalid sort type [not_a_valid_sort_type], sort types are [text, textNoCase, numeric]" );
171+
}
172+
});
173+
174+
it( "wrong arg type to struct.keyExists keeps the 'Can't cast Complex Object Type [Array] to String' message", function() {
175+
try {
176+
var s = { foo = 1, bar = 2 };
177+
s.keyExists( [ 1, 2, 3 ] );
178+
fail( "expected exception" );
179+
}
180+
catch ( any e ) {
181+
expect( e.type ).toBe( "expression" );
182+
expect( e.message ).toBe( "Can't cast Complex Object Type [Array] to String" );
183+
}
184+
});
185+
186+
it( "evaluate(valueList(q.missing_col)) keeps the 'Column [MISSING_COL] not found' message", function() {
187+
try {
188+
var q = queryNew( "name", "varchar", [ [ "alice" ], [ "bob" ] ] );
189+
evaluate( "valueList(q.missing_col)" );
190+
fail( "expected exception" );
191+
}
192+
catch ( any e ) {
193+
expect( e.type ).toBe( "database" );
194+
expect( e.message ).toBe( "Column [MISSING_COL] not found in query, Columns are [NAME]" );
195+
}
196+
});
197+
198+
it( "member call on null receiver keeps the 'variable [NULLVAR] doesn't exist' message", function() {
199+
try {
200+
var nullVar = javacast( "null", "" );
201+
nullVar.len();
202+
fail( "expected exception" );
203+
}
204+
catch ( any e ) {
205+
expect( e.type ).toBe( "expression" );
206+
expect( e.message ).toBe( "variable [NULLVAR] doesn't exist" );
207+
}
208+
});
209+
});
210+
});
211+
}
212+
}

0 commit comments

Comments
 (0)