Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
86a2765
[string.fmt] base implementation
sy-nico May 8, 2026
1aa2846
[string.fmt] flow form part 1
sy-nico May 8, 2026
6d7e969
[string.fmt] flow form part 2
sy-nico May 8, 2026
02d33b3
[string.fmt] LSP highlighting and autocomplete part 1
sy-nico May 8, 2026
8f99afc
[string.fmt] LSP highlighting and autocomplete part 2 (placeholder {}…
sy-nico May 8, 2026
84a9762
[string.fmt] implicit calling and internal = true
sy-nico May 8, 2026
08f6910
[string.fmt] remove unecessary scaffolding. fmt is no longer in the s…
sy-nico May 8, 2026
780775b
[string.fmt] formatting using codes, syntax highlighting, and handlin…
sy-nico May 9, 2026
045bdbf
[string.fmt] Register Flow fmt strings as synthetic functions
sy-nico May 9, 2026
ca260a3
[string.fmt] Simplify Format String Parser
sy-nico May 9, 2026
842295c
[string.fmt] Enable `\{` / `\}`
sy-nico May 9, 2026
b0f45d9
[string.fmt] Unify Parser Across LSP and Analyzer
sy-nico May 9, 2026
6f1fbad
[string.fmt] Deduplicate Raw String Helpers
sy-nico May 9, 2026
3ad2a1f
[string.fmt] Trim Duplication and Dead Code
sy-nico May 9, 2026
43e5d04
[string.fmt] fix
sy-nico May 9, 2026
5d68065
[string.fmt] tests
sy-nico May 9, 2026
c46ffda
[string.fmt] Allow Format Specs on Strings and Constants
sy-nico May 10, 2026
37bd8ff
[string.fmt] More tests!
sy-nico May 10, 2026
f95ef86
[string.fmt] documentation
sy-nico May 10, 2026
1bf3a0f
Merge remote-tracking branch 'origin/rc' into sy-4159-string-formatti…
sy-nico May 11, 2026
bf6dd61
SY-4159: Reviewer feedback
sy-nico May 12, 2026
db1e77a
SY-4159: Misc updates
sy-nico May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions arc/cpp/stl/str/str_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

#include <cmath>
#include <limits>
#include <memory>
#include <string>
Expand Down Expand Up @@ -247,6 +248,7 @@ TEST(StrModule, FromF32FormatsShortestRoundTrip) {
EXPECT_EQ(call_from<float>(f, "call_from_f32", 100.0f), "100");
EXPECT_EQ(call_from<float>(f, "call_from_f32", -2.5f), "-2.5");
EXPECT_EQ(call_from<float>(f, "call_from_f32", 42.5f), "42.5");
EXPECT_EQ(call_from<float>(f, "call_from_f32", std::copysign(0.0f, -1.0f)), "-0");
}

TEST(StrModule, FromF64FormatsShortestRoundTrip) {
Expand All @@ -259,6 +261,7 @@ TEST(StrModule, FromF64FormatsShortestRoundTrip) {
call_from<double>(f, "call_from_f64", 0.1234567890123456),
"0.1234567890123456"
);
EXPECT_EQ(call_from<double>(f, "call_from_f64", std::copysign(0.0, -1.0)), "-0");
}

TEST(StrModule, FromF64HandlesNaNAndInfinityWithGoCapitalization) {
Expand Down
5 changes: 4 additions & 1 deletion arc/go/analyzer/expression/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,10 @@ func analyzePrimary(ctx context.Context[parser.IPrimaryExpressionContext]) {
}
return
}
if ctx.AST.Literal() != nil {
if lit := ctx.AST.Literal(); lit != nil {
if rawStr := lit.STR_LITERAL_RAW(); rawStr != nil {
AnalyzeStringFmtLiteral(ctx, rawStr)
}
return
}
if expr := ctx.AST.Expression(); expr != nil {
Expand Down
88 changes: 88 additions & 0 deletions arc/go/analyzer/expression/string_fmt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2026 Synnax Labs, Inc.
//
// Use of this software is governed by the Business Source License included in the file
// licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with the Business Source
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

package expression

import (
"github.com/antlr4-go/antlr/v4"
"github.com/synnaxlabs/arc/analyzer/context"
"github.com/synnaxlabs/arc/analyzer/types"
"github.com/synnaxlabs/arc/fmtstring"
"github.com/synnaxlabs/arc/parser"
basetypes "github.com/synnaxlabs/arc/types"
"github.com/synnaxlabs/x/diagnostics"
"github.com/synnaxlabs/x/errors"
)

// AnalyzeStringFmtLiteral parses a STR_LITERAL_RAW token and analyzes its
// placeholders. Bypasses literal.ParseRawString so body offsets map to source
// bytes for per-placeholder diagnostic anchoring.
func AnalyzeStringFmtLiteral[T antlr.ParserRuleContext](
ctx context.Context[T],
rawStr antlr.TerminalNode,
) {
body, ok := fmtstring.StripDelimiters(rawStr.GetText())
if !ok {
ctx.Diagnostics.Add(diagnostics.Error(
errors.Newf("invalid raw string literal: %s", rawStr.GetText()), ctx.AST,
))
return
}
sym := rawStr.GetSymbol()
base := diagnostics.Position{Line: sym.GetLine(), Col: sym.GetColumn() + 1}
AnalyzeStringFmtSegments(ctx, body, base, ctx.AST)
}

// AnalyzeStringFmtSegments parses body and analyzes each placeholder expression
// in ctx's scope. base is the source position of body[0]; placeholder
// diagnostics anchor on the offending `{...}` span. Returns parsed segments,
// or nil if body is malformed.
func AnalyzeStringFmtSegments[T antlr.ParserRuleContext](
ctx context.Context[T],
body string,
base diagnostics.Position,
anchor antlr.ParserRuleContext,
) []fmtstring.Segment {
segments, err := fmtstring.Parse(body)
if err != nil {
ctx.Diagnostics.Add(diagnostics.Error(err, anchor))
return nil
}
for _, seg := range segments {
if !seg.IsPlaceholder {
continue
}
segStart := base.Advance(body, seg.Start)
segEnd := base.Advance(body, seg.End)
emit := func(d diagnostics.Diagnostic) {
ctx.Diagnostics.Add(d.WithRange(segStart, segEnd))
}
expr, diags := parser.ParseExpression(seg.Text)
if diags != nil && !diags.Ok() {
emit(diagnostics.Errorf(anchor,
"invalid placeholder expression %q: %s", seg.Text, diags.String()))
continue
}
Analyze(context.Child(ctx, expr))
t := types.InferFromExpression(context.Child(ctx, expr)).UnwrapChan()
if !t.IsNumeric() && t.Kind != basetypes.KindString {
emit(diagnostics.Errorf(anchor,
"placeholder %q has type %s; only numeric and string types are supported",
seg.Text, t))
continue
}
if seg.Spec == "" {
continue
}
if err := fmtstring.ValidateSpec(seg.Spec, t); err != nil {
emit(diagnostics.Error(err, anchor))
}
}
return segments
}
Loading
Loading