Skip to content

Commit 12100f3

Browse files
committed
LDEV-6303 drop duplicate re-fire, restore deep-copy contract; defer Hibernate LDEV-4121 fix to extension
1 parent 78ee088 commit 12100f3

5 files changed

Lines changed: 21 additions & 184 deletions

File tree

core/src/main/java/lucee/runtime/ComponentImpl.java

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
import lucee.runtime.component.ComponentLoader;
5858
import lucee.runtime.component.ComponentPageRef;
5959
import lucee.runtime.component.DataMember;
60-
import lucee.runtime.component.ExpressionDefaultSeeder;
6160
import lucee.runtime.component.ImportDefintion;
6261
import lucee.runtime.component.Member;
6362
import lucee.runtime.component.MetaDataSoftReference;
@@ -368,8 +367,6 @@ public ComponentImpl _duplicate(boolean deepCopy, boolean isTop) {
368367
if (useShadow) {
369368
addUDFS(trg, ((ComponentScopeShadow) scope).getShadow(), ((ComponentScopeShadow) trg.scope).getShadow());
370369
}
371-
372-
reFireExpressionDefaults(trg);
373370
}
374371
}
375372
finally {
@@ -379,41 +376,6 @@ public ComponentImpl _duplicate(boolean deepCopy, boolean isTop) {
379376
return trg;
380377
}
381378

382-
private static void reFireExpressionDefaults(ComponentImpl trg) {
383-
PageContext pc = ThreadLocalPageContext.get();
384-
if (pc == null) return;
385-
386-
Variables prevVars = pc.variablesScope();
387-
try {
388-
if (trg.scope instanceof Variables) pc.setVariablesScope((Variables) trg.scope);
389-
390-
ComponentImpl walker = trg;
391-
while (walker != null) {
392-
if (walker.pageSource != null) {
393-
try {
394-
lucee.runtime.Page page = walker.pageSource.loadPage(pc, false);
395-
if (page instanceof ExpressionDefaultSeeder) {
396-
try {
397-
((ExpressionDefaultSeeder) page)._seedExpressionDefaults(pc);
398-
}
399-
catch (Throwable t) {
400-
ExceptionUtil.rethrowIfNecessary(t);
401-
throw new lucee.runtime.exp.PageRuntimeException(Caster.toPageException(t));
402-
}
403-
}
404-
}
405-
catch (PageException pe) {
406-
throw new lucee.runtime.exp.PageRuntimeException(pe);
407-
}
408-
}
409-
walker = (ComponentImpl) walker.base;
410-
}
411-
}
412-
finally {
413-
if (prevVars != null) pc.setVariablesScope(prevVars);
414-
}
415-
}
416-
417379
private static void addUDFS(ComponentImpl trgComp, Map src, Map trg) {
418380
if (!src.isEmpty()) {
419381
Iterator it = src.entrySet().iterator();
@@ -2480,9 +2442,12 @@ else if (propOwnerPS == null) {
24802442
}
24812443

24822444
top.properties.properties.put(propNameLower, propImpl);
2483-
if (propImpl.getDefaultAsObject() != null) {
2445+
Object defaultObj = propImpl.getDefaultAsObject();
2446+
if (defaultObj != null && !(defaultObj instanceof lucee.runtime.component.ExpressionDefault)) {
2447+
// Expression-form defaults are seeded by the pseudo-constructor's inline emission
2448+
// (see TagProperty.emitExpressionEvalAndSet); only literal defaults are eager-set here.
24842449
Key propKey = propImpl.getNameAsKey();
2485-
scope.setEL(propKey, propImpl.getDefaultAsObject());
2450+
scope.setEL(propKey, defaultObj);
24862451
if (ownPropertyDefaults == null) ownPropertyDefaults = new HashSet<>();
24872452
ownPropertyDefaults.add(propKey);
24882453
}

core/src/main/java/lucee/runtime/component/ExpressionDefaultSeeder.java

Lines changed: 0 additions & 9 deletions
This file was deleted.

core/src/main/java/lucee/transformer/bytecode/PageImpl.java

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -380,17 +380,7 @@ public byte[] execute(String className) throws TransformerException {
380380
String[] interfaces = null;
381381
if (isComponent(comp)) {
382382
parent = ComponentPageImpl.class.getName();// "lucee/runtime/ComponentPage";
383-
List<TagProperty> exprDefProps = collectExpressionFormProperties(comp);
384-
boolean hasExprDefaults = !exprDefProps.isEmpty();
385-
if (isSub && hasExprDefaults) {
386-
interfaces = new String[] { SubPage.class.getName().replace('.', '/'), "lucee/runtime/component/ExpressionDefaultSeeder" };
387-
}
388-
else if (isSub) {
389-
interfaces = new String[] { SubPage.class.getName().replace('.', '/') };
390-
}
391-
else if (hasExprDefaults) {
392-
interfaces = new String[] { "lucee/runtime/component/ExpressionDefaultSeeder" };
393-
}
383+
if (isSub) interfaces = new String[] { SubPage.class.getName().replace('.', '/') };
394384
}
395385
else if (isInterface(comp)) parent = InterfacePageImpl.class.getName();// "lucee/runtime/InterfacePage";
396386
parent = parent.replace('.', '/');
@@ -775,8 +765,6 @@ else if (functions.length <= 10) {
775765
// newInstance/initComponent/call
776766
writeOutStatic(optionalPS, constr, keys, cw, comp, className);
777767

778-
writeOutSeedExpressionDefaults(optionalPS, constr, keys, cw, comp, className);
779-
780768
// set field subs
781769
FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE, "subs", "[Llucee/runtime/CIPage;", null, null);
782770
fv.visitEnd();
@@ -1561,47 +1549,6 @@ else if (defaultExpr instanceof LitBooleanImpl) {
15611549

15621550
}
15631551

1564-
private static List<TagProperty> collectExpressionFormProperties(TagCIObject component) {
1565-
List<TagProperty> result = new ArrayList<TagProperty>();
1566-
if (component == null || component.getBody() == null) return result;
1567-
List<Statement> statements = component.getBody().getStatements();
1568-
if (statements == null) return result;
1569-
for (Statement stmt: statements) {
1570-
if (!(stmt instanceof TagProperty)) continue;
1571-
TagProperty tagProp = (TagProperty) stmt;
1572-
Attribute defaultAttr = tagProp.getAttribute("default");
1573-
if (defaultAttr == null || defaultAttr.getValue() == null) continue;
1574-
Expression defaultExpr = defaultAttr.getValue();
1575-
if (defaultExpr instanceof LitStringImpl || defaultExpr instanceof LitNumberImpl || defaultExpr instanceof LitBooleanImpl) continue;
1576-
Attribute nameAttr = tagProp.getAttribute("name");
1577-
if (nameAttr == null || nameAttr.getValue() == null) continue;
1578-
result.add(tagProp);
1579-
}
1580-
return result;
1581-
}
1582-
1583-
private void writeOutSeedExpressionDefaults(PageSource optionalPS, ConstrBytecodeContext constr, Map<LitString, Integer> keys, ClassWriter cw, TagCIObject component,
1584-
String name) throws TransformerException {
1585-
List<TagProperty> exprDefProps = collectExpressionFormProperties(component);
1586-
if (exprDefProps.isEmpty()) return;
1587-
1588-
Method seedMethod = new Method("_seedExpressionDefaults", Types.VOID, new Type[] { Types.PAGE_CONTEXT });
1589-
GeneratorAdapter ga = new GeneratorAdapter(Opcodes.ACC_PUBLIC, seedMethod, null, new Type[] { Types.THROWABLE }, cw);
1590-
BytecodeContext bc = new BytecodeContext(config, optionalPS, constr, this, keys, cw, name, ga, seedMethod, writeLog(), suppressWSbeforeArg, output, returnValue,
1591-
sourceCode.getSourceOffset());
1592-
1593-
for (TagProperty tagProp: exprDefProps) {
1594-
Attribute nameAttr = tagProp.getAttribute("name");
1595-
Attribute defaultAttr = tagProp.getAttribute("default");
1596-
String propName = nameAttr.getValue() instanceof Literal ? ((Literal) nameAttr.getValue()).getString() : null;
1597-
if (propName == null) continue;
1598-
TagProperty.emitExpressionEvalAndSet(bc, propName, defaultAttr.getValue());
1599-
}
1600-
1601-
ga.returnValue();
1602-
ga.endMethod();
1603-
}
1604-
16051552
private String getTagAttributeValue(Tag tag, String attrName) {
16061553
Attribute attr = tag.getAttribute(attrName);
16071554
if (attr != null && attr.getValue() != null) {

core/src/main/java/lucee/transformer/bytecode/statement/tag/TagProperty.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,9 @@ public void _writeOut(BytecodeContext bc) throws TransformerException {
7575
bc.visitLine(tag.getEnd());
7676
}
7777

78-
/**
79-
* Emits eval + variablesScope.setEL bytecode for a single cfproperty expression-form default.
80-
* Shared by construction-time pseudo-constructor and the duplicate-time _seedExpressionDefaults
81-
* method so both paths emit identical eval bytecode and stay in lockstep.
82-
*/
78+
/** Emits eval + variablesScope.setEL bytecode for one cfproperty expression-form default,
79+
* inline in the pseudo-constructor. Slot is empty at this point — ComponentImpl skips
80+
* eager-setting the wrapper into scope at init for expression-form defaults. */
8381
public static void emitExpressionEvalAndSet(BytecodeContext bc, String propName, Expression defaultExpr) throws TransformerException {
8482
final GeneratorAdapter adapter = bc.getAdapter();
8583
defaultExpr.writeOut(bc, Expression.MODE_REF);

test/tickets/LDEV6303.cfc

Lines changed: 12 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -362,110 +362,46 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="orm" {
362362

363363
});
364364

365-
describe( "duplicate() re-fires expression-form defaults — fresh per-instance evaluation", function(){
365+
describe( "duplicate() — deep-copy semantic, no re-fire (option 6 contract)", function(){
366+
// Core duplicate is a deep copy of state, not a re-construction. Hibernate (which
367+
// needs fresh per-entity evaluation) handles its template→entity re-firing at the
368+
// extension layer. See test/functions/Duplicate.cfc for the broader user-mutation
369+
// preservation contract.
366370

367-
it( "duplicate fires fresh #now()# — dup gets a later timestamp than src after sleep", function(){
368-
// Contract: duplicate re-fires expression-form defaults so each duplicate
369-
// gets a fresh per-instance evaluation. The natural reading of
370-
// default="#now()#" is "the time this instance came into existence" — and
371-
// duplicates ARE new instances coming into existence.
372-
var src = new LDEV6303.ExpressionDefaultsCfc();
373-
sleep( 1100 );
374-
var dup = duplicate( src );
375-
376-
expect( dateCompare( dup.getNowDef(), src.getNowDef(), "s" ) ).toBeGT( 0,
377-
"duplicate must re-evaluate now() — dup's timestamp should be later than src's"
378-
);
379-
});
380-
381-
it( "duplicate fires fresh #createUUID()# — dup's UUID differs from src's", function(){
382-
var src = new LDEV6303.ExpressionDefaultsCfc();
383-
var dup = duplicate( src );
384-
385-
expect( dup.getUuidDef() ).notToBe( src.getUuidDef(),
386-
"duplicate must re-evaluate createUUID() — dup gets a fresh UUID"
387-
);
388-
// sanity: both look like UUIDs
389-
expect( len( dup.getUuidDef() ) ).toBeGT( 0 );
390-
expect( len( src.getUuidDef() ) ).toBeGT( 0 );
391-
});
392-
393-
it( "duplicate fires fresh deterministic expression (eval gives same value but separate computation)", function(){
394-
// Deterministic expressions naturally give the same value either way.
395-
// This test documents that they continue to work — fresh eval of
396-
// repeatString('x',5) gives "xxxxx" same as src's value.
371+
it( "deterministic expression-form value round-trips through duplicate as deep copy", function(){
397372
var src = new LDEV6303.ExpressionDefaultsCfc();
398373
var dup = duplicate( src );
399374
expect( dup.getExpressionDef() ).toBe( "xxxxx" );
400375
expect( dup.getExpressionDef() ).toBe( src.getExpressionDef() );
401376
});
402377

403-
it( "duplicate gives dup its own array — mutating dup does not leak to src", function(){
404-
// Mutable expression-form defaults (#[]#, #{}#) must produce a fresh
405-
// container per duplicate. Without re-fire, a shallow-copy duplicate
406-
// would share the array reference with src — silent leak.
378+
it( "deep duplicate gives dup its own array — mutating dup does not leak to src", function(){
379+
// Even without re-fire, deep duplicate must give dup a fresh array reference
380+
// (Duplicator.duplicate handles container deep-copy).
407381
var src = new LDEV6303.ExpressionDefaultsCfc();
408382
var dup = duplicate( src );
409383

410384
arrayAppend( dup.getFreshArrayDef(), "added-to-dup" );
411385

412386
expect( arrayLen( src.getFreshArrayDef() ) ).toBe( 0,
413-
"src's array must be unaffected by mutation on dup's array (separate references via re-fire)"
387+
"src's array must be unaffected by mutation on dup's deep-copy array"
414388
);
415389
expect( arrayLen( dup.getFreshArrayDef() ) ).toBe( 1 );
416390
});
417391

418-
it( "duplicate(src, false) — shallow duplicate also re-fires expression-form (Hibernate path)", function(){
419-
// Hibernate's CFCInstantiator uses cfc.duplicate(false) to create
420-
// entity instances. The shallow flag must NOT prevent re-fire of
421-
// expression-form defaults — otherwise every entity from a cached
422-
// template gets the template's frozen UUID/now/[]/{}.
423-
var src = new LDEV6303.ExpressionDefaultsCfc();
424-
var dup = duplicate( src, false );
425-
426-
expect( dup.getUuidDef() ).notToBe( src.getUuidDef(),
427-
"duplicate(src, false) must re-evaluate expression-form defaults too"
428-
);
429-
430-
// Mutation on dup's struct must not leak to src
431-
var s = dup.getNowStructDef();
432-
s.n = 999;
433-
434-
expect( src.getNowStructDef().n ).toBe( 1,
435-
"src's struct must be a separate reference from dup's (no shared-ref leak via shallow duplicate)"
436-
);
437-
});
438-
439-
it( "explicit assignment after duplicate still wins — user values aren't clobbered by re-fire", function(){
440-
// Re-fire seeds dup's scope at duplicate-time. Subsequent explicit
441-
// assignment by the user must override the re-fire'd value.
392+
it( "explicit assignment after duplicate wins over the deep-copied value", function(){
442393
var src = new LDEV6303.ExpressionDefaultsCfc();
443394
var dup = duplicate( src );
444395

445396
var freshTs = now();
446397
dup.setNowDef( freshTs );
447398

448-
expect( dateCompare( dup.getNowDef(), freshTs, "s" ) ).toBe( 0,
449-
"explicit setNowDef on dup must win over re-fire'd value"
450-
);
399+
expect( dateCompare( dup.getNowDef(), freshTs, "s" ) ).toBe( 0 );
451400
expect( dateCompare( src.getNowDef(), freshTs, "s" ) ).toBeLTE( 0,
452401
"src's nowDef must be unchanged by mutation on the duplicate"
453402
);
454403
});
455404

456-
it( "inherited expression-form default re-fires on the child duplicate", function(){
457-
// Child class instance, inheriting a #now()# default from parent.
458-
// duplicate of the child must re-fire the parent's expression-form
459-
// default — inheritance walk in _duplicate.
460-
var src = new LDEV6303.ChildExprDefCfc();
461-
sleep( 1100 );
462-
var dup = duplicate( src );
463-
464-
expect( dateCompare( dup.getParentNowDef(), src.getParentNowDef(), "s" ) ).toBeGT( 0,
465-
"inherited #now()# default must re-fire on the child's duplicate"
466-
);
467-
});
468-
469405
});
470406

471407
describe( "serializeJson shape — programmatic discriminability for consumers", function(){

0 commit comments

Comments
 (0)