StringBuilder in BoxLang: Fast Concatenation, Better &=, and Compile-Time Folding

This feature is part of Boxlang 1.15

String concatenation looks simple until it is on a hot path. In BoxLang, we now have a first-class BoxStringBuilder type, smarter concat operators, and compiler optimizations that reduce runtime work before your code even executes. This post walks through what changed, why it matters, and where BoxStringBuilder gives you measurable wins.

Performance Data

Benchmark input:

  • 100,000 iterations each run
  • Compare plain string concatenation growth versus StringBuilder.append()
  • Segment count from 2 through 8 strings

Key takeaways:

  • String concat is faster than string builder for just 2 strings
  • Break-even happens around 3 segments in this data.
  • Beyond that point, builder scales much better.
  • From 2 to 8 strings, concat growth is roughly 4.2x steeper than builder growth.
  • Therefore, Boxlang will automatically use string builder for any concat expressions with 4 expressions or more.

Why This Matters

In short-lived code paths, plain string concat is often fine. In loops and accumulators, repeated string creation can dominate runtime. String objects in Java are immutable, so joining two strings always creates a new string object and copies the contents of the old strings over. This can be wasteful if done many times in a row.

The current BoxLang behavior is now more intentional:

  • String concat remains ergonomic for simple cases.
  • BoxStringBuilder provides a mutable buffer for repeated appends.
  • The compiler folds contiguous string literals at compile time.
  • The compiler also rewrites explicit self-concat assignments into compound concat assignments where safe.

What Shipped

1) A first-class BoxStringBuilder type

You can create builders in three equivalent ways:

// New BIF -- 128 is the initial buffer capacity (characters)
sbA = stringBuilderNew( "Hello", 128 )

// Long literal form
sbB = stringbuilder{"Hello"}

// Short literal form (preferred in examples below)
sbC = sb{"Hello"}
sbC.append( " World" )
writeOutput( sbC.toString() )

Highlights:

  • Fluent mutable API: append, prepend, insert, delete, replace, reverse, clear, trim.
  • Silent cast to string when a string is required.
  • 1-based positional behavior for methods like insert/delete/replace/mid (BoxLang convention).
  • Can wrap a raw java.lang.StringBuilder and share the same underlying buffer.

Quick sb member method list:

  • append( value )
  • prepend( value )
  • insert( position, value )
  • delete( start, end )
  • replace( start, end, value )
  • reverse()
  • clear()
  • trim()
  • left( count )
  • right( count )
  • mid( start [, count ] )
  • find( substring [, start [, noCase ] ] )
  • contains( substring [, noCase ] )
  • startsWith( prefix )
  • endsWith( suffix )
  • length()
  • isEmpty()
  • toString()

Note, you can manually pass a BoxStringBuilder instance into any String BIF and we’ll cast it to a string, but there are now dedicated string builder BIF/member methods which preserve the string builder instance and simply operate on it directly.

2) &= optimization for mutable builders

Compound concat now uses in-place append semantics for BoxStringBuilder values.

sb = sb{"Hello"}

// desugars conceptually to: sb.append( " World" )
sb &= " World"

The key behavior is that sb is mutated in place instead of being replaced. This avoids repeated allocations and keeps references stable.

BoxLang applies the same in-place append path to raw java.lang.StringBuilder instances, not just BoxStringBuilder values.

3) Compiler rewrite for explicit self-assignment

The compiler recognizes explicit self-concat patterns like this:

data = data & chunk

and rewrites them into compound concat assignment semantics when the left target and first concat operand match structurally:

data &= chunk

This rewrite supports identifier, dot-access, and array-access targets (for example, variables.data and arr[ 1 ]).

4) Compile-time literal folding for concat chains

Contiguous string literals are combined during compilation.

result = "foo" & "bar" & "baz" & "qux"

is folded so literal segments are condensed up front, reducing runtime concat work.

It effectively turns into:

result = "foobarbazqux"

Mixed expressions are partially folded:

result = "foo" & "bar" & name & "baz" & "qux"

becomes effectively:

result = "foobar" & name & "bazqux"

at compile time.

Runtime Concat Strategy

At runtime, concat behavior is tiered:

  • 0 to 1 segment: trivial return.
  • 2 to 3 segments: direct String concat path.
  • 4 or more segments: StringBuilder-backed path with precomputed initial capacity.

That threshold is deliberate. It avoids overhead where direct concat is already competitive and switches to builder mode when segment counts grow.

This applies to mixed concat/interpolation statements such as:

result = "foo" & bar & "baz" & bum

or:

result = "foo#bar#baz#bum#"

Both forms are lowered into concat segments and effectively become this behind the scenes:

sb = new StringBuilder( precomputedCapacity )
sb.append( "foo" ).append( bar ).append( "baz" ).append( bum )
result = sb.toString()

Edge Cases and Behavior Notes

Null handling

Null segments are treated as empty values in concat/builder paths, which keeps concat flows robust.

sb literal accepts any expression

The value inside sb{ ... } can be any expression, not just quoted text. The expression is evaluated, then converted into a BoxStringBuilder value.

sb{ "Hello #name#" }
sb{ myVar }
sb{ getGreeting() }
sb{ foo.bar() }

Variable naming compatibility

A variable named sb or stringbuilder is still just a variable when used as an identifier:

sb = "not a builder"
stringbuilder = "also not a builder"

The parser only treats them specially in literal form when followed by braces: sb{ ... } or stringbuilder{ ... }.

Java interop nuance

BoxStringBuilder member methods are not injected onto a raw java.lang.StringBuilder. This avoids ambiguous behavior between BoxStringBuilder semantics (for example, 1-based positional methods) and Java StringBuilder semantics (0-based APIs).

If you want BoxStringBuilder methods on Java data, wrap the Java instance first:

javaSB = createObject( "java", "java.lang.StringBuilder" ).init( "Hello" )
boxSB = stringBuilderNew( javaSB )
boxSB.delete( 1, 1 ) // `BoxStringBuilder` semantics

Direct Java member calls still use Java semantics:

javaSB.delete( 2, 2 ) // Java no-op for equal start/end

Practical Guidance

Use plain concat when:

  • You are joining just a couple of values.
  • It is not in a hot loop.

Use BoxStringBuilder when:

  • You append repeatedly in loops.
  • You build large payloads incrementally (HTML, JSON fragments, logs, SQL snippets).
  • You want stable reference identity while mutating content.

Prefer compound concat:

  • Write data &= piece for mutable accumulation intent.
  • If legacy code uses data = data & piece, the compiler can now optimize that pattern in many common target shapes.

Closing

StringBuilder in BoxLang is not just a new type. It is an optimization surface across parser output, compiler transforms, and runtime operator behavior.

You still write idiomatic BoxLang. The platform just does more of the right thing under the hood.

1 Like