3.37. Performance Lint (perf_lint)
The perf_lint module detects common performance anti-patterns in daslang code
at compile time. When required, a lint pass runs after compilation and reports
warnings as CompilationError::performance_lint (error code 40217).
3.37.1. Quick start
Add require daslib/perf_lint to any file. The lint runs automatically at compile
time and reports warnings inline:
options gen2
require daslib/perf_lint
def process(data : string) : string {
var result = ""
for (i in range(100)) {
result += "x" // warning: PERF001
}
return result
}
3.37.2. Standalone utility
A standalone utility is available for batch-checking files from the command line:
bin/Release/daslang.exe utils/perf_lint/main.das -- file1.das file2.das [--quiet]
The utility compiles each file (without simulation or execution), runs the lint
visitor, and prints any warnings. Use --quiet to suppress progress messages.
Exit code is 1 if any files failed to compile, 0 otherwise.
3.37.3. Rules
3.37.3.1. PERF001 — string += in loop
String concatenation with += inside a loop creates O(n2) allocations.
Each iteration allocates a new string of increasing length, copying all previous content.
// Bad — O(n^2)
var result = ""
for (i in range(100)) {
result += "x" // PERF001
}
// Good — O(n)
let result = build_string() <| $(var writer) {
for (i in range(100)) {
write(writer, "x")
}
}
3.37.3.2. PERF002 — character_at in loop with loop variable
character_at(s, i) is O(n) per call because it internally calls strlen
to validate the index. In a loop iterating over string indices with the loop
variable as the index, this becomes O(n2) total.
// Bad — O(n^2)
for (i in range(length(s))) {
let ch = character_at(s, i) // PERF002
}
// Good — O(n) total, O(1) per access
peek_data(s) <| $(arr) {
for (i in range(length(arr))) {
let ch = int(arr[i])
}
}
3.37.3.3. PERF003 — character_at anywhere
Informational warning for any use of character_at. Each call does a bounds
check by scanning to the index. For accessing the first character, use
first_character which is O(1). For bulk access in hot paths, consider
peek_data for reads or modify_data for mutations.
let ch = character_at(s, 0) // PERF003 — use first_character(s) instead
let ch2 = first_character(s) // O(1), returns 0 for empty string
// Alternative: peek_data for O(1) indexed access
peek_data(s) <| $(arr) {
let ch = int(arr[0])
}
3.37.3.4. PERF004 — string interpolation reassignment in loop
str = "{str}{more}" inside a loop has the same O(n2) behavior as
str += "...". Each iteration allocates a new string containing all previous
content.
// Bad — O(n^2)
var result = ""
for (i in range(100)) {
result = "{result}x" // PERF004
}
// Good — O(n)
let result = build_string() <| $(var writer) {
for (i in range(100)) {
write(writer, "x")
}
}
3.37.3.5. PERF005 — length(string) in while condition
while (i < length(s)) recomputes strlen(s) on every iteration. If s
is not modified in the loop body, this is wasted work. Note that for loops
do not have this problem because for computes its source expression once.
// Bad — strlen every iteration
var i = 0
while (i < length(s)) { // PERF005
i ++
}
// Good — cached length
let slen = length(s)
var i = 0
while (i < slen) {
i ++
}
3.37.3.6. PERF006 — push/emplace in loop without reserve()
Calling push, push_clone, or emplace on an array inside a loop without
a preceding reserve() may trigger repeated reallocations as the array grows.
The rule traces through field access chains (self.items, data.buffer, etc.)
to find the root variable, and distinguishes different field paths — reserve(t.a, N)
does not suppress a warning for t.b |> push(x).
Conditional pushes (inside if/else) and loops with break/continue
are not flagged — the number of items is unpredictable, so reserve would be
guesswork.
// Bad — may realloc each iteration
var result : array<int>
for (i in range(1000)) {
result |> push(i) // PERF006
}
// Bad — field access, no reserve on this path
for (i in range(1000)) {
self.items |> push(i) // PERF006
}
// Good — pre-allocate
var result : array<int>
result |> reserve(1000)
for (i in range(1000)) {
result |> push(i)
}
// Good — conditional push, no warning
for (i in range(1000)) {
if (i > 500) {
result |> push(i)
}
}
// Good — loop with break, no warning
for (x in data) {
if (x == sentinel) {
break
}
result |> push(x)
}
3.37.3.7. PERF007 — unnecessary string(das_string) in comparison
das_string supports direct comparison with string literals and other
das_string values via == and !=. Wrapping in string() allocates
a new string unnecessarily.
// Bad — unnecessary allocation
if (string(name) == "foo") { ... } // PERF007
if (string(a) == string(b)) { ... } // PERF007
// Good — direct comparison
if (name == "foo") { ... }
if (a == b) { ... }
3.37.3.8. PERF008 — unnecessary get_ptr() for is/as
smart_ptr<Expression> and smart_ptr<TypeDecl> support is and as
type checks directly. Calling get_ptr() first to convert to a raw pointer
is unnecessary.
// Bad — get_ptr is redundant
if (get_ptr(expr) is ExprVar) { ... } // PERF008
var v = get_ptr(expr) as ExprCall // PERF008
// Good — direct type check
if (expr is ExprVar) { ... }
var v = expr as ExprCall
3.37.3.9. PERF009 — redundant move-init variable immediately returned
var x <- expr(); return <- x introduces an unnecessary intermediate variable.
The value is moved in and then immediately moved out. Simplify to
return <- expr().
// Bad — redundant variable
var inscope result <- make_thing()
return <- result // PERF009
// Good — direct return
return <- make_thing()
3.37.4. Suppressing specific warnings
To suppress a warning on a specific line, add a // nolint:PERFxxx comment
on the same line as the flagged expression:
let ch = character_at(s, idx) // nolint:PERF003 single indexed access, not a loop
The suppression is exact: // nolint:PERF003 only suppresses PERF003, not other
rules. The comment must appear after // on the same line that triggers the warning.
An optional explanation after the code is recommended but not required.
3.37.5. Important notes
Lint runs after optimization. The lint pass runs on the post-optimization AST. This means patterns in dead code (unused variables, unreachable functions) may not trigger warnings. In real code where results are used, the patterns are preserved and detected correctly.
ExprRef2Value wrapping. The compiler wraps many value-type reads in
ExprRef2Value nodes. The lint visitor unwraps these transparently — this is
an implementation detail, not something users need to worry about.
Closures are excluded. Code inside closures (blocks, lambdas) is not checked for loop-related patterns, since the closure may be called outside the loop context.
3.37.6. Tests
Tests are in utils/perf_lint/tests/:
bin/Release/daslang.exe dastest/dastest.das -- --test utils/perf_lint/tests
See also
daslib/perf_lint.das (source),
utils/perf_lint/main.das (standalone utility)