Skip to content

feat(ufmt): enhance printf functionnalities by adding flags #4136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 47 commits into
base: master
Choose a base branch
from

Conversation

paulogarithm
Copy link

@paulogarithm paulogarithm commented Apr 14, 2025

Fixes

  • ufmt/printf
    • added flag parsing, such as %.number, %#, %0, and others
    • cf 5d71b3a
    • new features has been tested using unit tests (see tests)

paulogarithm and others added 30 commits April 7, 2025 18:47
* includes gnominers
* includes home
* include pure packages
* reorganise gnominers into submodules
* create the dao submodule
* add the tofix folder with all broken features
* create the minerdao

/!\ DAO DOESNT WORK RN BECAUSE OF CYCLE DEPENDANCIES /!\
* i didnt use subfolders
* add precisision flag ("%.2f")
* add 0 & ' ' flags ("%03d", "% 3d")
* add length parsing (not used rn)
* add # for hex ("%#02x")
* add %i & %X (%i is just %d, %X is big hex)
* change the home
* ass some tests to handle what ive done
* also changed my home page so it displays a number
Format -> FormatRound

because it rounds...
* cause of major issue because it was reading to far
* better messages
* better formatting
* use `p/moun/md` for md formatting in users.gno
@paulogarithm
Copy link
Author

tell me if i need to add anything else !

Copy link
Member

@notJoon notJoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've confirmed that all the requested changes have been reflected and all tests have passed.

It's a minor point related to policy, but the "resolve conversation" button should not be pressed until the reviewer has checked it. Please refer to this document for more details.

Anyway, except for this, everything else looks good.

remove: review/triage-pending flag

@notJoon notJoon removed the review/triage-pending PRs opened by external contributors that are waiting for the 1st review label Apr 16, 2025
@leohhhn leohhhn moved this from In Progress to In Review in 🧑‍🎓Student contributors Apr 16, 2025
@notJoon
Copy link
Member

notJoon commented Apr 16, 2025

@paulogarithm

Hmm... When using ufmt (especially, using Sprintf function), gas consumption increases causing "out of gas" errors and test failures. The other CI failure issues will likely be resolved with a branch update, but this one seems to require code modifications.

I'll check this problem today. If it's okay, may I modify this code together with you?

@paulogarithm
Copy link
Author

paulogarithm commented Apr 16, 2025

totally fine for me, @notJoon ! but maybe it's because of the maps, so when you'll test, can you try putting the maps in the scope like i did, even if it's more greedy on memory ?

@notJoon
Copy link
Member

notJoon commented Apr 17, 2025

can you try putting the maps in the scope like i did, even if it's more greedy on memory ?

I'm testing by directly checking the gas consumption with txtar. It seems rather than map usage, we also need to improve parts related to memory allocation within the ufmt package overall.

@notJoon
Copy link
Member

notJoon commented Apr 17, 2025

@paulogarithm I couldn't push directly to this PR #4174, so I created a separate PR. Please check it and feel free to cherry-pick if needed. thank you 👍

@Kouteki Kouteki moved this from Triage to In Review in 🧙‍♂️gno.land core team Apr 17, 2025
Copy link
Member

@gfanton gfanton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice ! this is something really welcome !

Could you please update documentation above Sprintf as well as indicating all currently supported flags ? (you can take inspiration from https://pkg.go.dev/fmt)

While we are here we could support padding as well ? (could be done in another PR)

I've tried your code against some go/fmt test code, and the output differs in many formatting cases.

		// advanced int (from go/fmt)
		{"%.d", []any{0}, ""},
		{"%.0d", []any{0}, ""},
		{"% d", []any{12345}, " 12345"},
		{"%+d", []any{12345}, "+12345"},
		{"%+d", []any{-12345}, "-12345"},
		{"%#X", []any{0}, "0X0"},
		{"%x", []any{0x12abcdef}, "12abcdef"},
		{"%X", []any{0x12abcdef}, "12ABCDEF"},
		{"%x", []any{^uint32(0)}, "ffffffff"},
		{"%X", []any{^uint64(0)}, "FFFFFFFFFFFFFFFF"},
		// {"%010d", []any{12345}, "0000012345"},
		// {"%010d", []any{-12345}, "-000012345"}, // weird behaviors

		// advanced float (from go/fmt)
		{"%+.3e", []any{0.0}, "+0.000e+00"},
		{"%+.3e", []any{1.0}, "+1.000e+00"},
		{"%+.3x", []any{0.0}, "+0x0.000p+00"},
		{"%+.3x", []any{1.0}, "+0x1.000p+00"},
		{"%+.3f", []any{-1.0}, "-1.000"},
		{"%+.3F", []any{-1.0}, "-1.000"},
		{"%+.3F", []any{float32(-1.0)}, "-1.000"},
		{"% .3E", []any{-1.0}, "-1.000E+00"},
		{"% .3e", []any{1.0}, " 1.000e+00"},
		{"% .3X", []any{-1.0}, "-0X1.000P+00"},
		{"% .3x", []any{1.0}, " 0x1.000p+00"},
		{"%+.3g", []any{0.0}, "+0"},
		{"%+.3g", []any{1.0}, "+1"},
		{"%+.3g", []any{-1.0}, "-1"},
		{"% .3g", []any{-1.0}, "-1"},
		{"% .3g", []any{1.0}, " 1"},
		// Test sharp flag used with floats.
		{"%#g", []any{1e-323}, "1.00000e-323"},
		{"%#g", []any{-1.0}, "-1.00000"},
		{"%#g", []any{1.1}, "1.10000"},
		{"%#g", []any{123456.0}, "123456."},
		{"%#g", []any{1234567.0}, "1.234567e+06"},
		{"%#g", []any{1230000.0}, "1.23000e+06"},
		{"%#g", []any{1000000.0}, "1.00000e+06"},
		{"%#.0f", []any{1.0}, "1."},
		{"%#.0e", []any{1.0}, "1.e+00"},
		{"%#.0x", []any{1.0}, "0x1.p+00"},
		{"%#.0g", []any{1.0}, "1."},
		{"%#.0g", []any{1100000.0}, "1.e+06"},
		{"%#.4f", []any{1.0}, "1.0000"},
		{"%#.4e", []any{1.0}, "1.0000e+00"},
		{"%#.4x", []any{1.0}, "0x1.0000p+00"},
		{"%#.4g", []any{1.0}, "1.000"},
		{"%#.4g", []any{100000.0}, "1.000e+05"},
		{"%#.4g", []any{1.234}, "1.234"},
		{"%#.4g", []any{0.1234}, "0.1234"},
		{"%#.4g", []any{1.23}, "1.230"},
		{"%#.4g", []any{0.123}, "0.1230"},
		{"%#.4g", []any{1.2}, "1.200"},
		{"%#.4g", []any{0.12}, "0.1200"},
		{"%#.4g", []any{10.2}, "10.20"},
		{"%#.4g", []any{0.0}, "0.000"},
		{"%#.4g", []any{0.012}, "0.01200"},
		{"%#.0f", []any{123.0}, "123."},
		{"%#.0e", []any{123.0}, "1.e+02"},
		{"%#.0x", []any{123.0}, "0x1.p+07"},
		{"%#.0g", []any{123.0}, "1.e+02"},
		{"%#.4f", []any{123.0}, "123.0000"},
		{"%#.4e", []any{123.0}, "1.2300e+02"},
		{"%#.4x", []any{123.0}, "0x1.ec00p+06"},
		{"%#.4g", []any{123.0}, "123.0"},
		{"%#.4g", []any{123000.0}, "1.230e+05"},

Even if minimalist, this library should follow the output of go/fmt to be consistent with Go behaviors. Don't hesitate to follow / copy the fmt library code in Go to match the behavior.

Also i got some weird behaviours from %010d with -12345 -> 0000-12345

I will also check the gas issue. Since many projects import this package, we should take extra precautions regarding performance, but please do not compromise readability.

@@ -170,14 +295,14 @@ func (p *printer) doPrintf(format string, args []any) {
writeString(p, verb, arg)
case 'c':
writeChar(p, verb, arg)
case 'd':
case 'd', 'i':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%i doesn't seems to exist in go, I don't think we should port it in gno

}
stop := false
for {
x, ok := c2f[runes[*index]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hiding the reference inside a map complicates readability and performance. Why not use a direct switch case like this:

switch *index {
case '+'
case '-':
...
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it's special characters, just after checking if it exists, i use the internal value of the flag to set it as the flag

		x, ok := c2f[runes[*index]]
		switch {
		case ok:
			p.meta.flags |= x // here
			ptrForward(index, runes, 1)

so with map, i can check

  • if it's a flag modifier character
  • which flag do i need to trigger in the bit mask

maybe i can use a switch but idk if it will be better imo, what do you think ?

@thehowl
Copy link
Member

thehowl commented May 14, 2025

@paulogarithm did you have a chance to read guilhem's feedback?

@paulogarithm
Copy link
Author

I'm so sorry i was putting all my efforts on Flamegraphs & EVM, I did not made any updates, but I'm back !

  • @thehowl yep i'm reading each reviews
  • @gfanton i will add the new tests + improve the Sprintf doc + check your reviews
  • @notJoon ill cherry pick mostly everything because from what i seen, it seems that everything improves ufmt in a better way !

paulogarithm and others added 5 commits June 17, 2025 23:13
Co-authored-by: Guilhem Fanton <[email protected]>

* the tests are from @gfanton recommandation
* 45+ test fails -> 39 test fails
* todo: g flag, sign for scientific notations
ps: idk how to cherry pick so i copy pasted
Co-authored-by: notJoon <[email protected]>
@paulogarithm
Copy link
Author

alright so i added the tests from the real printf (the ones that @gfanton shared here) + i cherry picked (more like manually copy-pasted and put notJoon as co authored) the @notJoon 's code parts for optimization.
the only things that are mostly left on the tests are the G flag

@paulogarithm paulogarithm requested a review from gfanton June 20, 2025 22:27
type flag uint8

const (
flagHashtag flag = 1 << iota
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
flagHashtag flag = 1 << iota
flagHash flag = 1 << iota

@@ -32,13 +34,215 @@ func (b *buffer) writeRune(r rune) {
*b = utf8.AppendRune(*b, r)
}

// the different flags
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// the different flags
// bitmap representing different flags

lenH
lenL
lenLL
lenZ
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments would be good, maybe even full names.

Comment on lines +75 to +78
i := 0
for i < len(s) && unicode.IsDigit(rune(s[i])) {
i++
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
i := 0
for i < len(s) && unicode.IsDigit(rune(s[i])) {
i++
}
var i int
for idx, c := range s {
if c < '0' || c > '9' {
i = idx
return
}
}

Handles UTF-8 properly and more efficiently.

unicode.IsDigit considers many more digits: https://www.fileformat.info/info/unicode/category/Nd/list.htm

return num, i, true
}

var flagMap = [256]flag{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would maybe be better adjacent to their types.

Comment on lines +99 to +105
var lenMap = map[string]lengthModifier{
"ll": lenLL,
"l": lenL,
"h": lenH,
"hh": lenHH,
"z": lenZ,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused

Comment on lines +193 to +218
case 'h', 'l', 'z':
// Use lookahead to check for double-character length modifiers
// like 'hh' (char) or 'll' (long long) without iterating through options
if *index+1 < end {
next := runes[*index+1]
if c == 'h' && next == 'h' {
p.meta.length = lenHH
*index += 2 // Skip both characters
continue
} else if c == 'l' && next == 'l' {
p.meta.length = lenLL
*index += 2 // Skip both characters
continue
}
}

// Handle single-character length modifiers
switch c {
case 'h':
p.meta.length = lenH
case 'l':
p.meta.length = lenL
case 'z':
p.meta.length = lenZ
}
*index++
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The length flags don't really seem to be doing anything now, but I don't think they should anywaY

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📦 🤖 gnovm Issues or PRs gnovm related 🧾 package/realm Tag used for new Realms or Packages.
Projects
Status: In Review
Status: In Review
Development

Successfully merging this pull request may close these issues.

5 participants