BoxLang 1.15.0 is a high-impact release with two big headlines and a long tail of hardening. The first headline is a massive performance upgrade to string handling: a new first-class BoxStringBuilder type, compile-time literal folding, smarter &= semantics, and a runtime concat strategy that automatically switches to builder-backed accumulation once your expression gets big enough. Your existing string-heavy code just got faster. No rewrites required.
The second headline is architectural. Every class loader in the BoxLang runtime and module system has been fully abstracted behind a pluggable IClassLoaderFactory interface. That single seam is what unlocks the Android runtime and Ahead-of-Time (AOT) compiled runtimes such as GraalVM Native Image that we have on the roadmap. The default behavior is unchanged, but the ceiling just got a lot higher.
On top of that, 1.15.0 ships two new type-check BIFs (isBoxSet() and isRange()), a threadCurrent() BIF for direct JVM thread access, synchronized set support, security hardening for the web runtime, QoQ performance improvements, and an extensive round of CFML compatibility fixes, formatter corrections, and runtime hardening.
35 issues closed. 9 new features. 13 improvements. 13 bug fixes. Let's dig in.
🔤 Headline: BoxStringBuilder and Dramatically Faster String Concatenation
String concatenation looks simple until it lands on a hot path. Java strings are immutable, which means every a & b allocates a new object and copies both buffers. In a tight loop or a large accumulator, that dominates runtime.
BoxLang 1.15.0 fixes this at four layers simultaneously: a new type, a new operator semantic, a compiler pass, and an auto-switching runtime strategy. Together they mean your existing string code is faster without a single change, and when you want maximum control you now have a proper mutable builder to reach for.
The Performance Story
We benchmarked plain string concatenation against StringBuilder.append() across 100,000 iterations, with segment counts from 2 to 8 strings. The result:

Key takeaways:
- Plain concat is faster for exactly 2 strings.
- Break-even lands around 3 segments.
- From 2 to 8 strings, concat growth is roughly 4.2x steeper than builder growth.
- BoxLang now automatically switches to the builder path at 4 or more segments.
You get the fast path automatically. No opt-in. No new syntax.
A First-Class BoxStringBuilder Type
BoxStringBuilder wraps java.lang.StringBuilder with full BoxLang integration: member-function dispatch, 1-based positional semantics, and silent coercion to String wherever a string value is required.
Three equivalent creation forms:
// BIF: optional initial value and initial buffer capacity
sbA = stringBuilderNew( "Hello", 128 )
// Long literal form
sbB = stringbuilder{ "Hello" }
// Short literal form (preferred)
sbC = sb{ "Hello" }
sbC.append( " World" )
writeOutput( sbC.toString() ) // "Hello World"
The sb{ ... } literal accepts any expression, not just quoted text:
sb{ "Hello #name#" }
sb{ myVar }
sb{ getGreeting() }
sb{ foo.bar() }
And identifiers named sb or stringbuilder still work as normal variables. The parser only treats them as literals when followed by braces.
Fluent, mutable member API (1-based positional where applicable):
| Method | Description |
|---|---|
append( value ) | Append a value to the end |
prepend( value ) | Prepend a value to the beginning |
insert( position, value ) | Insert at a 1-based position |
delete( start, end ) | Delete characters from start to end (inclusive, 1-based) |
replace( start, end, value ) | Replace range with new value |
reverse() | Reverse the character sequence |
clear() | Reset to empty |
trim() | Remove leading/trailing whitespace |
left( count ) / right( count ) | Return leftmost/rightmost N characters |
mid( start [, count ] ) | Substring from 1-based position |
find( substring [, start [, noCase ] ] ) | Find a substring |
contains( substring [, noCase ] ) | Boolean membership |
startsWith( prefix ) / endsWith( suffix ) | Boundary checks |
length() / isEmpty() | State inspection |
toString() | Materialize as a Java String |
Building an HTML fragment in a loop, the way it should be done:
sb = sb{}
sb.append( "<ul>" )
for ( item in items ) {
sb.append( "<li>" ).append( item ).append( "</li>" )
}
sb.append( "</ul>" )
writeOutput( sb.toString() )
Silent string coercion. Pass a BoxStringBuilder anywhere a String is required and it converts automatically:
sb = sb{ "Hello " }
writeOutput( sb & "World" ) // "Hello World"
len( sb ) // works, sb is coerced
Optimization 1: &= Does In-Place Append
Compound concat now uses in-place append semantics for BoxStringBuilder values. No new allocation. Reference stays stable.
sb = sb{ "Hello" }
// &= desugars to sb.append( " World" )
sb &= " World"
writeOutput( sb ) // "Hello World"
The same in-place append also applies to raw java.lang.StringBuilder instances, not just BoxStringBuilder.
Optimization 2: Compiler Self-Assignment Rewrite
The compiler recognizes explicit self-concat patterns and rewrites them to the compound form automatically:
// Source:
data = data & chunk
// Compiled as:
data &= chunk
Applies to identifier, dot-access, and array-access targets: variables.data, arr[ 1 ], and friends. Legacy code that never learned the &= habit still gets the fast path.
Optimization 3: Compile-Time Literal Folding
Contiguous string literals are combined at compile time, before your code ever runs:
// Source:
result = "foo" & "bar" & "baz" & "qux"
// Compiled as:
result = "foobarbazqux"
Mixed expressions are partially folded, collapsing runs of adjacent literals:
// Source:
result = "foo" & "bar" & name & "baz" & "qux"
// Compiled as:
result = "foobar" & name & "bazqux"
Optimization 4: Auto-Switching Runtime Concat Strategy
At runtime, concat behavior is tiered by segment count:
| Segments | Strategy |
|---|---|
| 0 to 1 | Trivial return |
| 2 to 3 | Direct String concat path |
| 4 or more | StringBuilder-backed path with precomputed capacity |
This means expression chains like "foo" & bar & "baz" & bum and interpolation strings like "foo#bar#baz#bum#" are lowered automatically to:
sb = new StringBuilder( precomputedCapacity )
sb.append( "foo" ).append( bar ).append( "baz" ).append( bum )
result = sb.toString()
You write idiomatic BoxLang. The compiler and runtime cooperate to make it fast.
Java Interop Nuance
BoxStringBuilder member methods are not injected onto a raw java.lang.StringBuilder. Mixing 1-based (BoxLang) and 0-based (Java) positional semantics on the same instance would be a source of quiet bugs. Wrap the Java instance first if you want BoxLang semantics:
javaSB = createObject( "java", "java.lang.StringBuilder" ).init( "Hello" )
boxSB = stringBuilderNew( javaSB )
boxSB.delete( 1, 1 ) // BoxStringBuilder semantics (1-based)
javaSB.delete( 2, 2 ) // Java semantics (0-based, no-op for equal start/end)
📖 Deep dive resources:
- Syntax & Semantics guide: StringBuilder
- Community deep dive: StringBuilder in BoxLang: Fast Concatenation, Better &=, and Compile-Time Folding
🚀 Headline: Pluggable ClassLoader Factory (BL-2526)
BoxLang has always had ambitions beyond the JVM. We want BoxLang to run on Android. We want it to compile ahead-of-time via GraalVM Native Image. We want to support constrained and specialized execution targets that differ fundamentally from a standard JVM process.
What has stood in the way, everywhere, has been class loading.
On a standard JVM, loading a class means URLClassLoader.defineClass(), which JIT-compiles bytecode on first use. That model is wired into how BoxLang boots, how it compiles your .bx source, how modules get isolated, and how runtime-generated classes are resolved. On Android, URLClassLoader does not exist. On GraalVM Native Image in closed-world mode, you cannot call defineClass() at runtime at all.
In 1.15.0, every class-loader creation point in the runtime has been extracted behind a single IClassLoaderFactory interface. One seam governs three construction points:
- The runtime root class loader, parent of all module loaders, used for Java interop and dynamic lookups.
- Each module's isolated class loader, responsible for keeping module JARs from bleeding into the rest of the runtime.
- The generated class loader, which receives compiled BoxLang bytecode and resolves it for execution.
Swap one factory before the runtime boots. All three change together.
// Standard JVM (the default, no changes for existing deployments)
BoxRuntime.setClassLoaderFactory( new DynamicClassLoaderFactory() );
// Android: use the app ClassLoader as the runtime loader,
// DexClassLoader for modules, and a resolve-only loader for generated classes
BoxRuntime.setClassLoaderFactory( new AndroidClassLoaderFactory() );
// GraalVM Native Image (closed-world AOT):
// all classes pre-registered at build time, no defineClass() at runtime
BoxRuntime.setClassLoaderFactory( new NativeImageClassLoaderFactory() );
The default DynamicClassLoaderFactory is installed automatically and reproduces the exact behavior of every previous BoxLang release. Zero breaking changes for existing deployments.
What This Unlocks
| Target | What changes | BoxLang runtime | Your code |
|---|---|---|---|
| Standard JVM | Nothing | DynamicClassLoaderFactory (default) | Unchanged |
| Android (ART) | AndroidClassLoaderFactory at boot | App ClassLoader + DexClassLoader | Unchanged |
| GraalVM Native Image | NativeImageClassLoaderFactory at boot | Pre-registered classes, no defineClass | Unchanged |
| Custom OSGi / sandbox | Custom factory at boot | Full control | Unchanged |
This investment does not add a single new BIF or language construct. What it does is make BoxLang portable at the architecture level. The BoxLang code you write today will run, unchanged, on whatever runtime target we ship tomorrow.
For module authors: if your module currently constructs class loaders directly instead of going through the module service lifecycle, now is the time to align with the factory pattern. Reach out to the Ortus team for migration guidance.
✨ New Features
isBoxSet() BIF (BL-2506)
A dedicated type check to identify BoxLang BoxSet instances, rounding out the isArray() / isStruct() / isQuery() family.
s = setOf( 1, 2, 3 )
isBoxSet( s ) // true
isBoxSet( [ 1, 2, 3 ] ) // false, arrays are not sets
isBoxSet( "hello" ) // false
function processCollection( data ) {
if ( isBoxSet( data ) ) {
return data.toArray().sort( "numeric" )
}
if ( isArray( data ) ) {
return data.toSet().toArray().sort( "numeric" )
}
throw( "Unsupported collection type" )
}
isRange() BIF (BL-2507)
Test whether a value is a BoxLang Range instance.
r = 1..10
isRange( r ) // true
isRange( [ 1, 2, 3 ] ) // false
isRange( "1..10" ) // false, string representation, not a Range
function sumRange( val ) {
if ( !isRange( val ) ) {
throw( "Expected a Range, got #val.getClass().getSimpleName()#" )
}
return val.reduce( ( acc, item ) => acc + item, 0 )
}
threadCurrent() BIF (BL-2513)
Direct access to the current native Java thread from BoxLang.
javaThread = threadCurrent()
writeOutput( javaThread.getName() ) // java.lang.Thread getName()
writeOutput( javaThread.getState() ) // RUNNABLE, WAITING, etc.
writeOutput( javaThread.isVirtual() ) // true for virtual threads (Java 21+)
writeOutput( javaThread.threadId() ) // JVM thread identifier
Useful for telemetry, profiling integrations, and anywhere direct Thread API access is required.
Improved asString() on Class Instances (BL-2487)
Class instances now produce a more useful string representation. If the class defines a toString() method or property, that value is used. Otherwise the representation now includes the class name and a summary of public properties.
class Product {
property string name
property numeric price
function toString() {
return "Product(#variables.name#, $#variables.price#)"
}
}
p = new Product( name="Widget", price=9.99 )
writeOutput( p ) // Product(Widget, $9.99)
writeOutput( "#p#" ) // Product(Widget, $9.99)
Compile Validation for Inner Classes Inside Functions (BL-2490)
The compiler now emits a clear validation error when an inner class is declared inside a function body, instead of failing confusingly at runtime.
function doSomething() {
// Now produces a clear compile-time error:
class Helper { }
// "Inner classes cannot be declared inside a function body."
}
Auto-Deserialize JSON Args in Remote Methods (BL-2505)
Remote methods (marked access="remote") now automatically deserialize JSON string arguments into Struct or Array values when the argument type is declared as such.
remote function saveUser( required struct userData ) {
// Called via HTTP with userData={"name":"Alice","email":"alice@example.com"}
// userData is automatically deserialized to a BoxLang Struct:
writeOutput( userData.name ) // Alice
writeOutput( userData.email ) // alice@example.com
}
application/json Whitespace Compression (BL-2547)
The web runtime's whitespace compression, previously only active for HTML responses, now also applies to application/json responses. Smaller payloads, no application changes.
Error Basics in HTML Error Page Comment (BL-2204)
The default HTML error page now includes an HTML comment at the very top of the response with essential error information. Makes it possible to programmatically extract error details during testing and debugging even when full error display is suppressed.
<!-- BoxLang Error
Type : application.MyException
Message : Something went wrong
Template: /path/to/template.bxm
Line : 42
-->
Synchronized Set Support (BL-2494)
setNew() gains an isSynchronized boolean argument. When true, the set is wrapped in a thread-safe synchronized wrapper for safe concurrent access.
sharedSet = setNew( type="linked", isSynchronized=true )
thread name="t1" { sharedSet.add( "alpha" ) }
thread name="t2" { sharedSet.add( "beta" ) }
threadJoin( "t1,t2" )
writeOutput( sharedSet.size() ) // 2
🔧 Improvements
Language & Runtime
-
BL-2538 QoQ Performance Query of Queries executes significantly faster on large datasets, thanks to internal optimizations across the filter, sort, and aggregation pipeline.
-
BL-2539 Safe navigation in the expression interpreter The expression interpreter no longer throws when a safe navigation expression fails to resolve a key. It returns
nullinstead.
data = { name: "alice" }
val = data?.age?.years // null, no exception thrown
- BL-2543
nullis falsey but not booleanisBoolean( null )now correctly returnsfalse.nullis falsey but that does not make it a boolean value.
isBoolean( null ) // false (was: true in some earlier builds)
if ( !null ) { } // still works, null IS falsey
-
BL-2500 Struct keys capped in error messages
KeyNotFoundExceptionmessages now show a capped list of available keys to avoid flooding logs on large structs. -
BL-2501 Range empty-set semantics Paradoxical ranges (
5..3with a positive step) now produce zero elements instead of throwing.
r = ( 5..3 ).step( 1 )
r.toArray() // []
-
BL-2512 Formatter
arguments.separatorconfig Configurable spacing around named argument separators (default" = "). -
BL-2514 Session cookie gated on session management The web runtime no longer emits a
BXSESSIONIDcookie for apps with session management disabled. -
BL-2508 No path rewriting for known script extensions Avoids double-resolution in certain URL routing scenarios.
-
BL-2509 File upload copies rather than moves
fileUploadnow copies from temp to destination, preventing failures when the two paths cross filesystem mount boundaries. -
BL-2510 Stored proc
OUTparams respectnullEqualsEmptyStringCFML compat flag now honored, returning""instead ofnullwhere appropriate. -
BL-2497 CFML compat: string dates as numbers In CFML compatibility mode, string representations of dates can now participate in numeric operations, matching Adobe ColdFusion.
-
BL-2548 Adobe CF
returnFormatdrivesContent-TypeRemote methods withreturnFormat="json"now correctly setContent-Type: application/json. -
BL-2493 Set dump template handles Java sets The HTML dump template for
BoxSetnow also renders nativejava.util.Setinstances.
Security
- BL-2515 Web runtime blocks null-byte requests Any HTTP request with a null byte (
%00) in the URL is rejected with400 Bad Requestbefore processing, preventing null-byte injection attacks.
Upgrade Today
BoxLang 1.15.0 is available now.
# Install or upgrade via CommandBox
box install boxlang
box upgrade boxlang
# Or grab it directly from the download page
Key resources:
- 📖 Full 1.15.0 Release Notes
- 🔤 BoxStringBuilder Documentation
- 💬 StringBuilder Community Deep Dive
- 🌐 BoxLang Home
- 💰 BoxLang Plans
If you find issues, please report them on our issue tracker. If you love what's shipping, come tell us on the BoxLang Community forums or join us on Slack.
Happy building. Your strings just got a whole lot faster.
Add Your Comment