-
Notifications
You must be signed in to change notification settings - Fork 120
Expand file tree
/
Copy pathperf_lint.das
More file actions
2286 lines (2106 loc) · 104 KB
/
Copy pathperf_lint.das
File metadata and controls
2286 lines (2106 loc) · 104 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
options gen2
options indenting = 4
options strict_smart_pointers = true
module perf_lint shared private
//! Performance lint module.
//!
//! Detects common performance anti-patterns in daslang code at compile time.
//! When this module is required, a lint pass runs after compilation and reports
//! warnings as ``CompilationError::runtime_macro_performance`` (error code 31208).
//!
//! Rules:
//! PERF001 — string += in loop (O(n²))
//! PERF002 — character_at in loop with loop variable index (O(n) per call)
//! PERF003 — character_at anywhere (info: O(n) per call)
//! PERF004 — string interpolation reassignment in loop (O(n²))
//! PERF005 — length(string) in while condition (strlen each iteration)
//! PERF006 — push/emplace in loop without prior reserve()
//! PERF007 — unnecessary string(das_string) in comparison
//! PERF008 — unnecessary get_ptr() for is/as type checks
//! PERF009 — redundant move-init variable immediately returned
//! PERF010 — unnecessary get_ptr() for null comparison
//! PERF011 — unnecessary get_ptr() for field access
//! PERF012 — string(das_string) passed to strings module function
//! PERF013 — a += 1 / a -= 1 — use a++ / a--
//! PERF014 — closed-interval char-class range — see is_alpha / is_alnum / is_number etc.
//! PERF015 — ternary min/max — use min(a, b) / max(a, b)
//! PERF016 — ternary abs — use abs(x)
//! PERF017 — length(s) == 0 / != 0 — use empty(s) / !empty(s)
//! PERF018 — for (i in range(length(arr))) where i only indexes arr — use 'for (c in arr)'
//! PERF019 — int(T.a) | int(T.b) on bitfield-or-enum-with-operator-| — collapse to int(T.a | T.b)
//! PERF020 — T(x) where x is already T (workhorse type) — drop the redundant cast
//! PERF021 — cond ? T(a) : T(b) on workhorse cast T — hoist to T(cond ? a : b)
//! PERF022 — for (s in A) { B |> push(s) } / push_clone — use 'B |> push_from(A)' (bulk reserve+copy)
//! PERF023 — var X = clone_expression(E); ... $e(X) ... — qmacro splice already clones; drop the pre-clone
//! PERF024 — var X = clone_*(E); func(X) where func is [clone(...)]-annotated — callee clones; drop the pre-clone
//! PERF025 — string(...) inside string interpolation is redundant
//! PERF026 — heap traffic on a hot path ([hot_path] / [no_alloc])
//! PERF027 — environment lookup on a hot path ([hot_path] / [no_env])
//! PERF028 — console / file I/O on a hot path ([hot_path] / [no_io])
//!
//! PERF026-028 are annotation-gated: nothing is checked until a function declares a contract.
//! [cold_path] prunes the walk; @scratch on a field (or on a by-ref helper parameter) declares a
//! reused buffer whose sizing is intentional. See skills/perf_lint.md.
//!
//! The five markers are registered compiler-side (src/builtin/module_builtin_runtime.cpp, beside
//! [clone]), so code under contract requires NOTHING and pays no build time. This module is the
//! checker, and is deliberately not required by the code it checks — it runs wherever lint runs. — interpolation already converts to string
require daslib/ast_boost
require strings
require daslib/lint_config
// ---------------------------------------------------------------------------
// Visitor
// ---------------------------------------------------------------------------
struct VarStackEntry {
@do_not_delete v : Variable?
depth : int
is_iter : bool
}
struct Perf018State {
//! PERF018 per-for-loop candidate. Each enclosing `for` pushes one; `matched` flips true once we observe `for (i in range(length(V)))` with exactly one source and one iter var, where V is a bare Variable. We push a sentinel for non-matching loops too so visitExprFor can pop unconditionally and iter_var refs across nesting attribute to the right frame.
@do_not_delete iter_var : Variable?
@do_not_delete arr_var : Variable?
qualified_count : int
sources_seen : int
vars_seen : int
disqualified : bool
matched : bool
}
struct Perf022State {
//! PERF022 per-for-loop candidate. Detects 'for (s in A) { B |> push(s) }' (push and push_clone; all three pipe/dot/plain-call shapes compiler-fold to the same ExprCall). Pushed in preVisitExprFor in lockstep with PERF018.
@do_not_delete iter_var : Variable?
callee_id : int
sources_seen : int
vars_seen : int
disqualified : bool
matched : bool
}
struct Perf024Candidate {
//! PERF024 per-candidate state. One entry per `var X = clone_*(E)` decl seen in the current function (any of clone_expression / clone_type / clone_function / clone_variable / clone_structure). `safe_uses` counts ExprVar refs to X passed directly as an arg at a [clone(...)]-annotated position; `disqualified` is set on any other use. Reports the same way as PERF023: !disqualified && safe_uses >= 1.
@do_not_delete v : Variable?
safe_uses : int
disqualified : bool
}
struct Perf023Candidate {
//! PERF023 per-candidate state. One entry per `var X = clone_expression(E)` decl seen in the current function. `safe_uses` counts ExprVar refs to X whose ancestor chain (within this function) includes an `add_ptr_ref(...)` call — the splice-wrapper installed by `apply_qrules` for every `$e(...)` tag. `disqualified` is set on any other use (assignment, call arg passed to a non-splice consumer, etc.); the pre-clone is then load-bearing. The `var` carries the decl `at` directly (v.at), so the warning location is derived without storing LineInfo in the array.
@do_not_delete v : Variable?
safe_uses : int
disqualified : bool
}
enum private Perf025Kind {
// How a string()-cast value inside interpolation compares to direct
// interpolation, deciding whether/how PERF025 fires.
skip // not flaggable: value-shape change, or not a string() cast
equivalent // "{x}" == string(x): drop the cast
unsigned_hex // "{x}" is hex but string(x) is decimal: drop + ':d' hint
}
// ---------------------------------------------------------------------------
// PERF026-028 — hot-path contracts
// ---------------------------------------------------------------------------
bitfield private HotBits {
alloc // PERF026 — heap traffic (allocate or free)
env // PERF027 — environment lookup
io // PERF028 — console / file I/O
}
// LineInfo is not storable in an array, so each record carries the node and derives `.at` at warn
// time — the same trick PERF018/PERF023 use for their candidate stacks.
struct private HotSink {
bit : HotBits
what : string
@do_not_delete site : ExpressionPtr
}
struct private HotEdge {
@do_not_delete fn : Function?
@do_not_delete site : ExpressionPtr // the call site, which lives in the CALLER's body
}
// Per-function body-scan results, computed once per function per lint pass and sliced out of two
// flat arrays — a table of non-copyable summaries would need a clone per lookup.
struct private HotCache {
sinks_at : table<uint64; int2> // mangled-name hash -> (start, count) into sinks_all
edges_at : table<uint64; int2>
sinks_all : array<HotSink>
edges_all : array<HotEdge>
}
struct private HotFrame {
@do_not_delete fn : Function?
parent : int // index into the frame array; -1 at the root
@do_not_delete site : ExpressionPtr // where the parent called fn
// Deepest call site on this path that is still written in the root's own file. Reaching one
// callee from two such sites must report twice, so this is part of the dedup key below —
// keying on the function alone would hide every call site after the first.
@do_not_delete anchor : ExpressionPtr
}
def private hot_bits_of(func : Function?) : HotBits {
var bits : HotBits
for (a in func.annotations) {
if (a.annotation.name == "hot_path") {
bits = HotBits.alloc | HotBits.env | HotBits.io
} elif (a.annotation.name == "no_alloc") {
bits.alloc = true
} elif (a.annotation.name == "no_env") {
bits.env = true
} elif (a.annotation.name == "no_io") {
bits.io = true
}
}
return bits
}
def private is_cold_path(func : Function?) : bool {
for (a in func.annotations) {
if (a.annotation.name == "cold_path") return true
}
return false
}
// Every array/table heap op bottoms out in a __builtin_array_* / __builtin_table_* extern, so the
// prefix (minus the probe/lock forms, which never touch the heap) covers push/reserve/resize/erase/
// insert/delete without naming one surface generic — and a NEW builtin is caught rather than missed.
def private is_alloc_builtin(n : string) : bool {
if (starts_with(n, "__builtin_array_") || starts_with(n, "__builtin_table_")) {
return !(ends_with(n, "_lock") || ends_with(n, "_lock_mutable")
|| ends_with(n, "_unlock") || ends_with(n, "_unlock_mutable")
|| n == "__builtin_table_find" || n == "__builtin_table_key_exists"
|| n == "__builtin_table_get_key" || n == "__builtin_table_keys"
|| n == "__builtin_table_values")
}
return n == "clone_string"
}
// Container ops whose destination may be a reused buffer. Matched on the ORIGIN name so every
// generic instance of push/resize/... resolves to the surface spelling the author wrote.
def private is_container_sizing(n : string) : bool {
return (n == "resize" || n == "resize_no_init" || n == "reserve" || n == "clear"
|| n == "push" || n == "push_clone" || n == "emplace" || n == "push_from"
|| n == "push_clone_from" || n == "insert" || n == "erase" || n == "pop")
}
// True when `expr` reaches a declaration marked @scratch — a struct field or a module global
// (`var @scratch g : array<T>`, the clear()-recycled capture-rail shape) — walking through
// indexing and deref, so `p.kblobs[b]` counts by virtue of kblobs.
def private is_scratch_dest(var expr : Expression?) : bool {
return false if (expr == null)
if (expr is ExprField) {
var f = expr as ExprField
var fd = f.field
if (fd != null) {
for (a in fd.annotation) {
if (a.name == "scratch") return true
}
}
return is_scratch_dest(f.value)
}
if (expr is ExprVar) {
var v = (expr as ExprVar).variable
if (v != null) {
for (a in v.annotation) {
if (a.name == "scratch") return true
}
// a ref binding (`var lst & = pool.free_bufs[b]`) carries its destination's scratch-ness
if (v._type != null && v._type.flags.ref && v.init != null) {
return is_scratch_dest(v.init)
}
}
return false
}
if (expr is ExprRef2Value) return is_scratch_dest((expr as ExprRef2Value.subexpr))
if (expr is ExprAt) return is_scratch_dest((expr as ExprAt.subexpr))
if (expr is ExprSafeAt) return is_scratch_dest((expr as ExprSafeAt.subexpr))
if (expr is ExprCast) return is_scratch_dest((expr as ExprCast.subexpr))
return false
}
// a helper sizing a caller's reused buffer marks the PARAMETER (destination arrives by ref)
def private takes_scratch_arg(fn : Function?) : bool {
return false if (fn == null)
for (arg in fn.arguments) {
for (a in arg.annotation) {
if (a.name == "scratch") return true
}
}
return false
}
def private is_env_builtin(n : string) : bool {
return n == "get_env_variable" || n == "has_env_variable" || n == "set_env_variable"
}
def private is_io_builtin(n : string) : bool {
return (n == "print" || n == "to_log" || n == "fprint" || n == "fopen" || n == "fclose"
|| n == "fread" || n == "fwrite" || n == "fflush" || n == "popen" || n == "popen_argv")
}
// Scans one function body: the sinks it performs directly, plus its direct call edges. Indirect
// calls (ExprInvoke) are unresolvable — annotate the implementations they reach instead. ExprAddr
// is deliberately NOT an edge: taking a function's address is not calling it.
class HotBodyScan : AstVisitor {
sinks : array<HotSink>
edges : array<HotEdge>
// Depth inside a panic(...) argument list. A panic is fatal in daslang — not an exception — so
// its message is on the abort path, and the interpolation building it is not hot-path traffic.
panic_depth : int = 0
// Depth inside a macro-generated subtree. A rewritten stub force_at-stamps the CALLER's line
// onto its splice, so its machinery would be blamed on every call site. Macros mark only the
// ROOT of their output generated (ast_match.das does), hence the depth rather than a flag test.
gen_depth : int = 0
def override preVisitExpression(var expr : ExpressionPtr) : void {
if (expr.genFlags.generated) {
gen_depth++
}
}
def override visitExpression(var expr : ExpressionPtr) : ExpressionPtr {
if (expr.genFlags.generated) {
gen_depth--
}
return expr
}
def add_sink(bit : HotBits; what : string; var site : ExpressionPtr) {
return if (panic_depth > 0 || gen_depth > 0)
sinks |> push(HotSink(bit = bit, what = what, site = site))
}
def override visitExprCall(var expr : ExprCall?) : ExpressionPtr {
if (expr.func != null && expr.func.name == "panic") {
panic_depth--
}
return expr
}
def override preVisitExprCall(var expr : ExprCall?) : void {
if (expr.func == null) return
let n = string(expr.func.name)
if (n == "panic") {
panic_depth++
return
}
return if (panic_depth > 0)
// @scratch = declared intent, at the field or on a by-ref helper's parameter
let origin = string(expr.func.fromGeneric != null ? expr.func.fromGeneric.name : expr.func.name)
if (!empty(expr.arguments)
&& ((is_container_sizing(origin) && is_scratch_dest(expr.arguments[0]))
|| takes_scratch_arg(expr.func))) {
return
}
if (is_env_builtin(n)) {
add_sink(HotBits.env, "{n}()", expr)
} elif (is_io_builtin(n)) {
add_sink(HotBits.io, "{n}()", expr)
} elif (is_alloc_builtin(n)) {
add_sink(HotBits.alloc, "{n}()", expr)
} elif (expr.func.flags.builtIn && expr.func.result != null
&& expr.func.result.baseType == Type.tString) {
add_sink(HotBits.alloc, "{n}() returns a newly allocated string", expr)
}
edges |> push(HotEdge(fn = expr.func, site = expr))
}
def override preVisitExprNew(var expr : ExprNew?) : void {
add_sink(HotBits.alloc, "new", expr)
}
// `new Foo(field = v)` is an ExprAscend (move-to-heap) around a make-struct, not an ExprNew
def override preVisitExprAscend(var expr : ExprAscend?) : void {
add_sink(HotBits.alloc, "new", expr)
}
def override preVisitExprDelete(var expr : ExprDelete?) : void {
add_sink(HotBits.alloc, "delete", expr)
}
def override preVisitExprStringBuilder(var expr : ExprStringBuilder?) : void {
add_sink(HotBits.alloc, "string interpolation", expr)
}
// table[key] inserts a default entry when the key is missing, so it allocates on READ too;
// the ?[] form (ExprSafeAt) has its own visitor hook and never inserts. A @scratch table
// (the pool / residency-cache shape) is declared reuse — indexing it is the owner's strategy.
def override preVisitExprAt(var expr : ExprAt?) : void {
if (expr.subexpr == null || expr.subexpr._type == null) return
if (expr.subexpr._type.baseType == Type.tTable && !is_scratch_dest(expr.subexpr)) {
add_sink(HotBits.alloc, "table index (inserts a default entry when the key is missing)", expr)
}
}
// A lambda captures into a heap frame; a plain block does not.
def override preVisitExprMakeBlock(var expr : ExprMakeBlock?) : void {
if (expr.mmFlags.isLambda) {
add_sink(HotBits.alloc, "lambda capture frame", expr)
}
}
}
class PerfLintVisitor : AstVisitor {
compile_time_errors : bool
// variable + loop tracking
loop_depth : int = 0
in_closure : int = 0
var_stack : array<VarStackEntry>
scope_stack : array<int>
// while tracking
in_while_cond : bool = false
// counter-based detection state
in_character_at_call : int = 0
@do_not_delete current_character_at : ExprCall?
in_length_while_call : int = 0
in_string_builder_check : int = 0
@do_not_delete string_builder_target_var : Variable?
@do_not_delete string_builder_save_stack : array<Variable?>
// reported character_at locations (to avoid duplicate PERF002+PERF003)
@do_not_delete reported_character_at : array<ExprCall?>
// PERF006: path keys for arrays that had reserve() called
reserved_paths : array<string>
// PERF006: if-depth per scope — push(0) on scope entry, pop on exit
if_depth_stack : array<int>
// current function being visited (for inferStack reporting)
@do_not_delete current_function : Function?
in_template : bool = false
// PERF026-028: functions in this module carrying [hot_path] / [no_alloc] / [no_env] / [no_io],
// collected during the main pass so the contract scan needs no traversal of its own
@do_not_delete hot_roots : array<Function?>
// PERF006: known-length loop tracking
known_length_loop_depth : int = 0
current_for_known_length : bool = true
// PERF006: suppress when loop has break/continue (unpredictable iteration count)
loop_has_early_exit : array<bool>
// PERF009: track last move-initialized variable for redundant return detection
@do_not_delete pending_move_var : Variable?
@safe_when_uninitialized pending_move_at : LineInfo
// PERF009: true when the pending var was clone-initialized (`var x := expr`,
// lowered to `<- clone_to_move(...)`) — the collapse is `return clone_to_move`,
// not `return <- expr` (which would move/destroy the source).
pending_move_is_clone : bool = false
// PERF018: per-for-loop index-only candidates; arr_path (string) and the owning ExprFor (for `.at` at warn time) live in parallel arrays kept in lockstep with perf018_stack. at_qualifying_pushes records how many increments each in-flight ExprAt added to in_qualifying_idx so visitExprAt can undo them.
perf018_stack : array<Perf018State>
perf018_paths : array<string>
@do_not_delete perf018_for_exprs : array<ExprFor?>
in_qualifying_idx : int = 0
at_qualifying_pushes : array<int>
// PERF022: per-for-loop bulk-push candidates; same lockstep with for-loop
// nesting. `array<T>` requires T trivially-storable, so the owning ExprFor
// pointer lives in a parallel `perf022_for_exprs` array.
perf022_stack : array<Perf022State>
@do_not_delete perf022_for_exprs : array<ExprFor?>
// PERF023: per-function candidates for `var X = clone_expression(E)`. Cleared in preVisitFunction; finalized in visitFunction. `perf023_splice_depth` is a counter incremented when visiting INTO an `add_ptr_ref(...)` call (post-expansion form of `$e(...)` splice tags) and decremented on exit; an ExprVar reference to a candidate that fires while the counter is >0 is a "safe use" (apply_template will clone the substitution anyway), otherwise the candidate is disqualified.
perf023_candidates : array<Perf023Candidate>
perf023_splice_depth : int = 0
// PERF024: per-function candidates for `var X = clone_*(E)` whose only uses are direct args at [clone(...)]-annotated positions. `perf024_skip_iptr` holds the intptr identity of a Variable that's about to be visited via a pre-classified annotated-arg position; cleared in preVisitExprVar so that ExprVar visit doesn't re-classify (would otherwise disqualify).
perf024_candidates : array<Perf024Candidate>
perf024_skip_iptr : uint64 = 0ul
warning_count : int = 0
// collection mode — when true, warnings are appended to `warnings` instead of to_log
collect_warnings : bool = false
warnings : array<string>
issues : array<LintIssue> // structured twin of `warnings`, same entries in the same order
// dedup: reported (file_hash, line, column) to suppress duplicate warnings from generic instantiations
reported_locations : table<uint64>
// PERF019: cache of enum-types we've probed for an `operator |` overload.
// Keys are intptr(Enumeration?); vals say "yes, has operator|".
perf019_enum_or_probed : array<uint64>
perf019_enum_or_yes : array<uint64>
def PerfLintVisitor() {
pass
}
// The file is part of the key: the PERF026-028 chains report sinks in OTHER files, so a
// line|column key alone silently drops a finding that happens to share a position elsewhere.
def location_key(at : LineInfo) : uint64 {
let pos = uint64(at.line) | (uint64(at.column) << uint64(20))
return pos ^ (intptr(at.fileInfo) * 1099511628211ul)
}
// Filters: disabled (denylist) and enabled (whitelist; empty == all).
// Caller-populated via collect overload; applied alongside // nolint suppression.
disabled_codes : table<string>
enabled_codes : table<string>
def is_suppressed_code(code : string) : bool {
return (key_exists(disabled_codes, code)
|| (!empty(enabled_codes) && !key_exists(enabled_codes, code)))
}
def is_suppressed(text : string; at : LineInfo) : bool {
let code = extract_lint_code(text)
if (!empty(code)) {
return true if (key_exists(disabled_codes, code)
|| (!empty(enabled_codes) && !key_exists(enabled_codes, code)))
}
return is_lint_suppressed(at, code)
}
def perf_warning(text : string; at : LineInfo) : void {
// Suppress in templates and inside macro-generated functions — the user didn't write that code.
if (in_template
|| (current_function != null && current_function.flags.generated)) return
// Deduplicate warnings — same source location reported once, regardless of generic instantiations
let key = location_key(at)
if (key_exists(reported_locations, key)) return
reported_locations |> insert(key)
// Check inline suppression: // PERFxxx on the same line
if (is_suppressed(text, at)) return
warning_count++
var msg = text
if (current_function != null && current_function.fromGeneric != null && !empty(current_function.inferStack)) {
msg = build_string() $(var w) {
w |> write(text)
w |> write("\n while compiling {current_function.name}")
for (ih in current_function.inferStack) {
w |> write("\n instanced from {ih.func.name} at {describe(ih.at)}")
}
}
}
if (compile_time_errors) {
compiling_program() |> macro_performance_warning(at, msg)
} elif (collect_warnings) {
warnings |> push("performance warning: {msg} at {describe(at)}")
issues |> push(make_lint_issue(msg, at))
} else {
to_log(LOG_WARNING, "performance warning: {msg} at {describe(at)}\n")
}
}
// --- variable scope helpers ---
def is_loop_variable(v : Variable?) : bool {
if (v == null) return false
for (entry in var_stack) {
if (entry.v == v && entry.is_iter) return true
}
return false
}
def is_defined_outside_loop(v : Variable?) : bool {
if (v == null || loop_depth == 0 || is_loop_variable(v)) return false
for (entry in var_stack) {
if (entry.v == v) return entry.depth < loop_depth
}
// not on stack — function argument or global — outside any loop
return true
}
// --- expression helpers ---
def is_character_at_zero(expr : ExprCall?) : bool {
if (length(expr.arguments) >= 2) {
var idx = expr.arguments[1]
if (idx is ExprConstInt && (idx as ExprConstInt).value == 0) return true
}
return false
}
def is_array_func(expr : ExprCall?; fname : string) : bool {
// Builtin generics (push, reserve, etc.) have fromGeneric set.
if (expr.func == null || expr.func.fromGeneric == null
|| expr.func.fromGeneric.name != fname || empty(expr.arguments)) return false
let arg = expr.arguments[0]
return arg._type != null && arg._type.baseType == Type.tArray
}
def find_expr_path(var expr : Expression?; var path : string&) : Variable? {
//! Walks expression chains (field access, deref, index, etc.) to find the root variable, building a path string to distinguish foo.a from foo.b.
return null if (expr == null)
if (expr is ExprVar) {
var evar = expr as ExprVar
return null if (evar.variable == null)
return evar.variable
}
if (expr is ExprRef2Value) return find_expr_path((expr as ExprRef2Value.subexpr), path)
if (expr is ExprField) {
var field = expr as ExprField
path = ".{field.name}{path}"
return find_expr_path(field.value, path)
}
if (expr is ExprSafeField) {
var field = expr as ExprSafeField
path = ".{field.name}{path}"
return find_expr_path(field.value, path)
}
if (expr is ExprAt) {
var at = expr as ExprAt
path = "[*]{path}"
return find_expr_path(at.subexpr, path)
}
if (expr is ExprSafeAt) {
var at = expr as ExprSafeAt
path = "[*]{path}"
return find_expr_path(at.subexpr, path)
}
if (expr is ExprSwizzle) {
var swiz = expr as ExprSwizzle
return find_expr_path(swiz.value, path)
}
if (expr is ExprCast) {
var ca = expr as ExprCast
return find_expr_path(ca.subexpr, path)
}
if (expr is ExprRef2Ptr) {
var rr = expr as ExprRef2Ptr
return find_expr_path(rr.subexpr, path)
}
if (expr is ExprPtr2Ref) {
var rr = expr as ExprPtr2Ref
return find_expr_path(rr.subexpr, path)
}
// Bail on everything else (Op3, NullCoalescing, calls, etc.)
return null
}
def make_path_key(v : Variable?; path : string) : string {
return "{intptr(v)}{path}"
}
def find_string_var_from_expr(expr : Expression?) : Variable? {
return null if (expr == null)
// Unwrap ExprRef2Value (compiler inserts these for value-type reads)
var inner = expr
if (inner is ExprRef2Value) {
inner = (inner as ExprRef2Value.subexpr)
}
return null if (inner == null || !(inner is ExprVar))
var evar = inner as ExprVar
return null if (evar.variable == null || evar.variable._type == null)
if (evar.variable._type.baseType == Type.tString) return evar.variable
return null
}
// --- PERF007/PERF008 helpers ---
def is_string_of_das_string(expr : Expression?) : bool {
//! Returns true if expr is a string(X) call where X is das_string.
return false if (expr == null)
var inner = expr
if (inner is ExprRef2Value) {
inner = (inner as ExprRef2Value.subexpr)
}
return false if (inner == null || !(inner is ExprCall))
var call = inner as ExprCall
// After inference, string() calls may have FakeContext/FakeLineInfo extra args.
if (empty(call.arguments) || call.name != "string") return false
var arg = call.arguments[0]
if (arg._type == null || arg._type.baseType != Type.tHandle) return false
return arg._type.annotation != null && arg._type.annotation.name == "das_string"
}
def is_get_ptr_of_smart_ptr(expr : Expression?) : bool {
//! Returns true if expr is a X call where X is a smart_ptr.
return false if (expr == null || !(expr is ExprCall))
var call = expr as ExprCall
if (call.func == null || call.func.fromGeneric == null) return false
return call.func.fromGeneric.name == "get_ptr"
}
def is_get_ptr_vs_null(maybe_get_ptr : Expression?; maybe_null : Expression?) : bool {
//! Returns true if maybe_get_ptr is get_ptr() and maybe_null is null.
if (!is_get_ptr_of_smart_ptr(maybe_get_ptr) || maybe_null == null) return false
if (maybe_null is ExprConstPtr) {
var cptr = maybe_null as ExprConstPtr
return cptr.value == null
}
return false
}
// --- generic constant / structural helpers (PERF013-017) ---
def is_const_zero(expr : Expression? const) : bool {
if (expr == null) return false
if (expr is ExprConstInt) return (expr as ExprConstInt).value == 0
if (expr is ExprConstUInt) return (expr as ExprConstUInt).value == 0u
if (expr is ExprConstInt64) return (expr as ExprConstInt64).value == 0l
if (expr is ExprConstUInt64) return (expr as ExprConstUInt64).value == 0ul
if (expr is ExprConstFloat) return (expr as ExprConstFloat).value == 0.0f
if (expr is ExprConstDouble) return (expr as ExprConstDouble).value == 0.0lf
return false
}
def is_const_one(expr : Expression? const) : bool {
if (expr == null) return false
if (expr is ExprConstInt) return (expr as ExprConstInt).value == 1
if (expr is ExprConstUInt) return (expr as ExprConstUInt).value == 1u
if (expr is ExprConstInt64) return (expr as ExprConstInt64).value == 1l
if (expr is ExprConstUInt64) return (expr as ExprConstUInt64).value == 1ul
if (expr is ExprConstFloat) return (expr as ExprConstFloat).value == 1.0f
if (expr is ExprConstDouble) return (expr as ExprConstDouble).value == 1.0lf
return false
}
def is_const_neg_one(expr : Expression? const) : bool {
if (expr == null) return false
if (expr is ExprConstInt) return (expr as ExprConstInt).value == -1
if (expr is ExprConstInt64) return (expr as ExprConstInt64).value == -1l
if (expr is ExprConstFloat) return (expr as ExprConstFloat).value == -1.0f
if (expr is ExprConstDouble) return (expr as ExprConstDouble).value == -1.0lf
return false
}
def expr_equal_struct(a : Expression? const; b : Expression? const; require_pure : bool = true) : bool {
//! Structural equality via describe(). With `require_pure=true` (default), returns false when either side has side effects — protects rules that suggest collapsing duplicated subexpressions (PERF014/015/016) from silently changing evaluation count.
if ((a == null || b == null)
|| (require_pure && (!a.flags.noSideEffects || !b.flags.noSideEffects))) return false
return describe(a) == describe(b)
}
def is_workhorse_numeric(t : TypeDecl?) : bool {
//! True if the type is one of the six numeric workhorse scalars (int, uint, int64, uint64, float, double). Vectors/bitfields/enums do NOT qualify.
if (t == null) return false
let bt = t.baseType
return (bt == Type.tInt || bt == Type.tUInt
|| bt == Type.tInt64 || bt == Type.tUInt64
|| bt == Type.tFloat || bt == Type.tDouble)
}
def is_collection_length_call(expr : Expression? const) : bool {
//! True for length(string) / length(das_string) / length(array) / length(table).
if (expr == null || !(expr is ExprCall)) return false
let call = expr as ExprCall
if (call.func == null || call.func.name != "length") return false
let modname = string(call.func._module.name)
return modname == "strings" || modname == "$" || modname == "builtin"
}
// --- PERF019: int(T.a) | int(T.b) cast-collapse helpers ---
def unwrap_int_cast(expr : Expression? const) : Expression? const {
//! If `expr` is `int(x)` (single argument, no fake/context args), return x.
if (expr == null) return null
var inner = expr
if (inner is ExprRef2Value) {
inner = (inner as ExprRef2Value.subexpr)
}
if (inner == null || !(inner is ExprCall)) return null
let call = inner as ExprCall
if (call.func == null || empty(call.arguments)) return null
let fname = string(call.func.fromGeneric != null ? call.func.fromGeneric.name : call.func.name)
if (fname != "int") return null
return call.arguments[0]
}
def enum_has_or_operator(t : TypeDecl?) : bool {
//! Returns true if any module in the compiling program defines `def operator |(a, b : T) : ...` for the enum represented by `t`. Result is cached on perf019_enum_or_probed / _yes by intptr(t.enumType) so repeated checks across a large file don't re-walk the function table.
if (t == null || t.enumType == null) return false
let key = intptr(t.enumType)
for (idx in range(length(perf019_enum_or_probed))) {
return perf019_enum_or_yes |> has_value(key) if (perf019_enum_or_probed[idx] == key)
}
var found = false
program_for_each_module(compiling_program()) $(mod) {
return if (found)
for_each_function(mod, "|") $(var fn) {
if (length(fn.arguments) == 2
&& fn.arguments[0]._type != null && fn.arguments[1]._type != null
&& (fn.arguments[0]._type |> is_same_type(t, RefMatters.no, ConstMatters.no, TemporaryMatters.no))
&& (fn.arguments[1]._type |> is_same_type(t, RefMatters.no, ConstMatters.no, TemporaryMatters.no))) {
found = true
}
}
}
perf019_enum_or_probed |> push(key)
if (found) {
perf019_enum_or_yes |> push(key)
}
return found
}
def inner_type_supports_or(t : TypeDecl?) : bool {
//! True if `T | T` resolves: bitfields always; enums only when an `operator |` overload exists. Vectors/ints/etc. don't get here — the caller restricts to int-cast inputs whose underlying type is either bitfield or enum.
if (t == null) return false
if (t.baseType == Type.tBitfield) return true
if (t.baseType == Type.tEnumeration) return enum_has_or_operator(t)
return false
}
// --- PERF020: redundant same-name workhorse cast helpers ---
def perf020_target_basetype(fname : string) : Type {
//! Map a workhorse cast-function name to the Type it produces.
if (fname == "int") return Type.tInt
if (fname == "int8") return Type.tInt8
if (fname == "int16") return Type.tInt16
if (fname == "int64") return Type.tInt64
if (fname == "uint") return Type.tUInt
if (fname == "uint8") return Type.tUInt8
if (fname == "uint16") return Type.tUInt16
if (fname == "uint64") return Type.tUInt64
if (fname == "float") return Type.tFloat
if (fname == "double") return Type.tDouble
if (fname == "string") return Type.tString
if (fname == "bitfield") return Type.tBitfield
if (fname == "bitfield8") return Type.tBitfield8
if (fname == "bitfield16") return Type.tBitfield16
if (fname == "bitfield64") return Type.tBitfield64
return Type.none
}
// --- PERF024: redundant pre-clone before [clone(...)]-annotated function helpers ---
def is_clone_init_call(expr : Expression const?) : bool {
//! True when expr is a single-arg call to one of the AST clone primitives.
if (expr == null || !(expr is ExprCall)) return false
let c = expr as ExprCall
if (length(c.arguments) != 1) return false
let n = string(c.name)
return (n == "clone_expression" || n == "clone_type"
|| n == "clone_function" || n == "clone_variable"
|| n == "clone_structure")
}
def get_clone_param_indices(callee : Function?; var out : array<int>&) : void {
//! Fills `out` with the param-index list for any [clone(...)] annotations on callee. Caller-owned out array avoids array return ownership.
out |> clear
if (callee == null) return
for (ann in callee.annotations) {
if (ann == null || ann.annotation == null || ann.annotation.name != "clone") continue
for (a in ann.arguments) {
for (i in 0 .. length(callee.arguments)) {
if (callee.arguments[i].name == a.name) {
out |> push(i)
break
}
}
}
}
}
// --- PERF025: redundant string(...) inside string interpolation helpers ---
def perf025_value_kind(arg : Expression const?) : Perf025Kind {
//! Classifies a string() cast's value argument by how `"{x}"` (the string builder's own conversion) compares to `string(x)`:
if (arg == null || arg._type == null) return Perf025Kind.skip
let bt = arg._type.baseType
if (bt == Type.tInt || bt == Type.tInt8 || bt == Type.tInt16 || bt == Type.tInt64
|| bt == Type.tFloat || bt == Type.tDouble || bt == Type.tString
|| (bt == Type.tHandle && arg._type.annotation != null
&& arg._type.annotation.name == "das_string")) return Perf025Kind.equivalent
if (bt == Type.tUInt || bt == Type.tUInt8 || bt == Type.tUInt16 || bt == Type.tUInt64) return Perf025Kind.unsigned_hex
return Perf025Kind.skip
}
def perf025_string_call_value(e : Expression const?) : Expression const? {
//! If `e` is a `string(value)` cast call, return its value argument (arguments[0]). Skips an explicit `string(x, true)` hex request — the second argument being a literal `true` means the user wants hex, which no longer matches plain interpolation for signed types. The float / double / string / das_string overloads have no hex param, so their arguments[1] (a FakeContext) is not an ExprConstBool and passes through.
if (e == null || !(e is ExprCall)) return null
let c = e as ExprCall
if (c.func == null || empty(c.arguments)) return null
let fname = string(c.func.fromGeneric != null ? c.func.fromGeneric.name : c.func.name)
if (fname != "string") return null
if (length(c.arguments) >= 2) {
let a1 = c.arguments[1]
if (a1 != null && a1 is ExprConstBool && (a1 as ExprConstBool).value) return null
}
return c.arguments[0]
}
def perf025_unwrap_element(elem : Expression const?) : Expression const? {
//! Peels one ExprRef2Value off an interpolation element so a `string(...)` call read through a value-context wrapper is still exposed. A format spec (`"{x:fmt}"`) lowers to `_::fmt(":fmt", x)`, but `fmt` has no string-valued overload, so `string(x)` can never sit under a fmt wrapper (it would not compile) — no fmt unwrap is needed.
var ex = elem
if (ex != null && ex is ExprRef2Value) {
ex = (ex as ExprRef2Value.subexpr)
}
return ex
}
// --- function tracking ---
def override preVisitFunction(var fn : FunctionPtr) : void {
current_function = fn
in_template = fn.moreFlags.isTemplate
// PERF026-028: a template carries no resolved callees, so only real functions are roots
if (!in_template && uint(hot_bits_of(fn)) != 0u) {
hot_roots |> push(fn)
}
// PERF023: reset per-function candidate set.
perf023_candidates |> clear
perf023_splice_depth = 0
// PERF024: reset per-function candidate set.
perf024_candidates |> clear
perf024_skip_iptr = 0ul
}
def override visitFunction(var fn : FunctionPtr) : FunctionPtr {
// PERF023: report any candidate whose uses are all splice-safe.
for (c in perf023_candidates) {
if (!c.disqualified && c.safe_uses >= 1 && c.v != null) {
perf_warning(
"PERF023: var initialized via clone_expression and only spliced into qmacro/qmacro_block/qmacro_expr/qmacro_block_to_array; the splice already clones (apply_template), so drop the pre-clone and inline the source expression",
c.v.at)
}
}
// PERF024: report any candidate whose only uses are annotated-arg-safe.
for (c in perf024_candidates) {
if (!c.disqualified && c.safe_uses >= 1 && c.v != null) {
perf_warning(
"PERF024: var initialized via clone_* and only passed to a function annotated [clone(...)] at the matching arg position; the callee clones internally, so drop the pre-clone and inline the source",
c.v.at)
}
}
perf023_candidates |> clear
perf023_splice_depth = 0
perf024_candidates |> clear
perf024_skip_iptr = 0ul
current_function = null
in_template = false
return <- fn
}
// --- scope tracking ---
def override preVisitExprBlock(blk : ExprBlock?) : void {
if (blk.blockFlags.isClosure) {
in_closure++
string_builder_save_stack |> push(string_builder_target_var)
string_builder_target_var = null
}
scope_stack |> push(length(var_stack))
}
def override visitExprBlock(var blk : ExprBlock?) : ExpressionPtr {
if (blk.blockFlags.isClosure) {
in_closure--
if (!empty(string_builder_save_stack)) {
string_builder_target_var = string_builder_save_stack |> back()
string_builder_save_stack |> pop()
}
}
if (!empty(scope_stack)) {
var_stack |> resize(scope_stack |> back())
scope_stack |> pop()
}
return <- blk
}
// --- variable declaration tracking ---
def override preVisitExprLetVariable(expr : ExprLet?; var v : VariablePtr; last : bool) : void {
if (in_closure == 0) {
var_stack |> push(VarStackEntry(v = v, depth = loop_depth, is_iter = false))
}
// PERF023: seed candidate when init is `clone_expression(...)`.
if (in_closure == 0 && v != null && v.init != null && v.init is ExprCall) {
let initCall = v.init as ExprCall
if (initCall.name == "clone_expression" && length(initCall.arguments) == 1) {
perf023_candidates |> push(Perf023Candidate(v = v, safe_uses = 0, disqualified = false))
}
}
// PERF024: seed candidate when init is any clone_* primitive. Broader than PERF023's clone_expression-only seed because the [clone(...)] contract covers all five AST clone primitives. Same closure-gating rationale as PERF023.
if (in_closure == 0 && v != null && is_clone_init_call(v.init)) {
perf024_candidates |> push(Perf024Candidate(v = v, safe_uses = 0, disqualified = false))
}
// PERF009: track last move-initialized variable. Two flavors:
if (last && v.flags.init_via_move && v.init != null) {
var dominated_by_clone = false
var ini = v.init
if (ini is ExprCall) {
let ecall = ini as ExprCall
if (ecall.func != null) {
let fname = string(ecall.func.fromGeneric != null ? ecall.func.fromGeneric.name : ecall.func.name)
dominated_by_clone = fname == "clone_to_move"
}
}
pending_move_var = v
pending_move_at = expr.at
pending_move_is_clone = dominated_by_clone
}
}
// --- PERF009: redundant move-init variable before return ---
def override preVisitExprBlockExpression(blk : ExprBlock?; expr : ExpressionPtr) : void {
if (pending_move_var != null) {
var e = expr
if (e is ExprReturn) {
var ret = e as ExprReturn
if (ret.returnFlags.moveSemantics && ret.subexpr != null) {
var sub = ret.subexpr
if (sub is ExprRef2Value) {
sub = (sub as ExprRef2Value.subexpr)
}
if (sub is ExprVar && (sub as ExprVar.variable) == pending_move_var) {
if (pending_move_is_clone) {
perf_warning("PERF009: redundant clone into variable immediately returned; use return clone_to_move(expr) directly", pending_move_at)
} else {
perf_warning("PERF009: redundant move into variable immediately returned; use return <- expr directly", pending_move_at)
}
}
}
}
pending_move_var = null
}
}
// --- loop tracking ---
def override preVisitExprFor(expr : ExprFor?) : void {
if (in_closure == 0) {
scope_stack |> push(length(var_stack))
loop_depth++
if_depth_stack |> push(0)
current_for_known_length = true
loop_has_early_exit |> push(false)
perf018_push_sentinel(expr)
perf022_push_sentinel(expr)
}
}
def is_known_length_source(src : Expression?) : bool {
if (src == null || src._type == null) return false
let bt = src._type.baseType
// any source whose length is determined at the start of the loop
return (bt == Type.tArray || bt == Type.tString
|| bt == Type.tRange || bt == Type.tRange64
|| bt == Type.tURange || bt == Type.tURange64
|| bt == Type.tFixedArray)
}
def override preVisitExprForSource(expr : ExprFor?; src : ExpressionPtr; last : bool) : void {
if (in_closure == 0 && !is_known_length_source(src)) {
current_for_known_length = false
}
if (in_closure == 0) {
perf018_record_source(src)
perf022_record_source(src)
}
}
def override preVisitExprForBody(expr : ExprFor?) : void {
if (in_closure == 0 && current_for_known_length) {
known_length_loop_depth++
}
if (in_closure == 0) {
perf018_finalize_on_body()
perf022_inspect_body(expr.body)
}
}
def override preVisitExprForVariable(expr : ExprFor?; var v : VariablePtr; last : bool) : void {
if (in_closure == 0) {
var_stack |> push(VarStackEntry(v = v, depth = loop_depth - 1, is_iter = true))
perf018_record_var(v)
perf022_record_var(v)
}
}
def override visitExprFor(var expr : ExprFor?) : ExpressionPtr {