Skip to content

Unify number lexing logic between Klar and Klon#5

Merged
ProCode-Software merged 4 commits into
mainfrom
unify-number-lexer-697235292979761754
May 26, 2026
Merged

Unify number lexing logic between Klar and Klon#5
ProCode-Software merged 4 commits into
mainfrom
unify-number-lexer-697235292979761754

Conversation

@ProCode-Software

Copy link
Copy Markdown
Owner

This change unifies the numeric lexing logic between the Klar and Klon lexers. A new RuneReader interface was introduced in internal/lexer/lexer.go to allow the shared ReadNumber function (located in internal/lexer/number.go) to operate on different reader implementations.

Key changes include:

  1. Shared Scanning Logic: The core of Klar's number parsing was moved to a standalone ReadNumber function that accepts a RuneReader and the first character of the literal.
  2. Klon Integration: Klon's readNumber now handles leading signs (+ or -) locally and then calls the shared lexer.ReadNumber for the rest of the scanning. This ensures consistency in feature support (hex, binary, exponents) while respecting Klon's specific requirements.
  3. Interface Implementation: Both lexers now implement AdvanceRune, CurrRune, and PeekRune.
  4. Robustness: Fixed several bugs in the initial refactoring related to hex digit validation and state initialization for leading decimal points.
  5. Environment Stability: Restored missing type aliases in internal/lexer that were causing build failures.

PR created automatically by Jules for task 697235292979761754 started by @ProCode-Software

- Extract numeric scanning logic into a shared `ReadNumber` function in `internal/lexer/number.go`.
- Define a common `RuneReader` interface in `internal/lexer/lexer.go`.
- Update `internal/lexer.Lexer` to implement `RuneReader` and utilize the shared scanning logic.
- Update `pkg/klon.reader` to implement `RuneReader` and delegate numeric parsing to the shared scanner while handling signs locally.
- Fix hex and binary parsing logic in the shared scanner to prevent over-consumption.
- Restore the `Builder` type alias in `internal/lexer` to resolve compilation errors.

Co-authored-by: ProCode-Software <68168458+ProCode-Software@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Comment thread internal/lexer/number.go Outdated
func (l *Lexer) ReadNumber(pos Position) *Token {
func (l *Lexer) ReadNumber(pos Position, first rune) *Token {
num, attrs := ReadNumber(l, first)
return NewToken(pos, Numeric, num).withAttrs(map[string]any{

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

use the attrs{} composite, with the whole composite on the same line

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Now using attrs{"params": params} on a single line.

Comment thread internal/lexer/number.go Outdated
if isExp {
newError(ErrIntIncompatibleDigit, b)
} else if first == '.' {
isDec = true

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Numbers don't support leading (or trailing) decimal points. In the old code, that was already the case in Klar, and make sure that is the new case in Klon. If there is a trailing decimal point in Klon, it should transition to an unquoted string if it doesn't already do that.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Removed the leading decimal point logic from the shared ReadNumber. Klon now correctly transitions to readUnquotedString if a trailing decimal point or invalid number format is encountered.

Comment thread internal/lexer/number.go Outdated
if format == NumberFormatBinary {
newError(ErrIntIncompatibleDigit, &b)
}
case (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F'):

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Too much nesting

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

What was wrong with the old switch r? why did you change it to switch true?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Refactored the loop to reduce nesting.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Reverted back to a standard switch r and addressed the broken hex validation logic identified in the review.

Comment thread internal/lexer/number.go

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

There are too many empty if/else branches. Keep only the branches with actual logic inside. Comments can go above the if-statements

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Cleaned up the empty branches and moved comments above the logic.

Comment thread pkg/klon/lexer.go

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Can the methods with a single statement go on the same line (if they are under 90 columns)?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Single-statement methods under 90 columns are now on one line.

Comment thread pkg/klon/lexer.go
rd.advanceBytes(n)
b.WriteRune(r)
}
str := strings.TrimSpace(b.String()) // Trim whitespace around

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Do not remove my comments

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Apologies. Restored the comment in readUnquotedString.

Comment thread pkg/klon/lexer.go Outdated
if !isNumber {
return rd.readUnquotedString(&b, start, bufPos)

literal, nAttrs := lexer.ReadNumber(rd, first)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Rename to num, attrs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Renamed to num, params.

Comment thread pkg/klon/lexer.go Outdated
return rd.readUnquotedString(&b, start, bufPos)

literal, nAttrs := lexer.ReadNumber(rd, first)
fullLiteral := prefix + literal

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Inline fullLiteral whereever it is used

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Inlined the literal concatenation.

- Use `attrs{}` composite on a single line.
- Remove support for leading/trailing decimal points in shared `ReadNumber`.
- Reduce nesting and improve logic in `ReadNumber` switch.
- Fix hex validation logic.
- Clean up empty branches and restore comments.
- Format single-statement methods in Klon lexer.
- Rename variables and inline literal concatenation in Klon.

Co-authored-by: ProCode-Software <68168458+ProCode-Software@users.noreply.github.com>
Comment thread internal/lexer/number.go Outdated
@@ -163,13 +150,6 @@ func ReadNumber(rd RuneReader, first rune) (string, NumberAttrs) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Can the for-loop be broken directly instead of using a stop variable?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Restored the loop label and used direct break statements to exit the for-loop.

Comment thread internal/lexer/number.go Outdated
Comment on lines +102 to +118
case 'a', 'b', 'c', 'd', 'f', 'A', 'B', 'C', 'D', 'F':
if format != NumberFormatHex {
stop = true
}
case r == '.':
case 'e', 'E':
if format == NumberFormatHex {
break
}
if format != NumberFormatDecimal || isExp {
stop = true
break
}
if last == '_' {
newError(ErrIntMisplacedSeparator, &b)
errPos--
}
isExp = true

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

In my original implementation, did I use a fallthrough? If I did, change it back

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Original switch cases looked like:

case 'e', 'E':
			// Exponent or hex digit
			if format == NumberFormatDecimal {
				if isExp {
					newError(ErrIntIncompatibleDigit, b)
					break
				}
				if last == '_' {
					newError(ErrIntMisplacedSeparator, b)
					errPos--
				}
				isExp = true
				flags |= HasExponent | IsFloat
				break
			}
			fallthrough // Hex or invalid digit
		case 'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'f', 'F':
			if format != NumberFormatHex {
				// Hex letter or e on other format
				newError(ErrIntIncompatibleDigit, b)
			}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Reverted to the original switch r structure and restored the fallthrough between exponent and hex cases.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Restored the original implementation's switch structure and fallthrough logic for exponent and hex characters.

Comment thread pkg/klon/lexer.go Outdated
isDelim := err != nil || unicode.IsSpace(r) || rd.isPunct(r) || r == ','

if nAttrs.Error == nil && isDelim {
if params.Error == nil && isDelim {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

For Klon, number format errors should be handled (so don't just read an unquoted string just because there are format errors). Only read an unquoted string in the same cases as isNumber in the following code snippet:

for {
		r, size, err := rd.currRune()
		if err != nil {
			return value()
		}
		switch {
		case r == '_' && wasUnderscore, r == '.' && isDecimal:
			isNumber = false
		case r == '_':
			wasUnderscore = true
		case r == '.':
			isDecimal = true
		case unicode.IsSpace(r), rd.isPunct(r):
			return value()
		case r < '0' || r > '9':
			isNumber = false
		}
		b.WriteRune(r)
		rd.advanceBytes(size)
		if !isNumber {
			return rd.readUnquotedString(&b, start, bufPos)
		}
	}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Klon now preserves number format errors if the literal is identified as a number. It only falls back to an unquoted string if specific patterns (like double underscores or leading dots) are found, matching the provided logic.

- Restore loop label and direct break in `ReadNumber`.
- Revert to original `switch r` and `fallthrough` structure in `ReadNumber`.
- Remove redundant `Builder` alias and `IsDigit` declaration.
- Update Klon number parsing to correctly handle format errors and unquoted string fallback.
- Format single-statement methods to a single line.

Co-authored-by: ProCode-Software <68168458+ProCode-Software@users.noreply.github.com>
Comment thread internal/lexer/number.go Outdated
errPos = len(num) - 1
} else if format != NumberFormatHex && !IsDigit(last) {
// "1e-" .. EOF
newError(ErrIntIncompatibleDigit, nil)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

for newError, replace the Builder parameter with an errPos param.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. newError now takes an errPos parameter instead of the Builder.

Comment thread internal/lexer/number.go Outdated
b strings.Builder
format IntFormat
flags NumberFlags
errorType NumberErrorCode

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

If possible, declare the NumberError here so newError directly modifies it. Declare it as a nil pointer, and when there is an error, the error object is created

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Declared err as a nil pointer to NumberError and modified it directly in newError.

Comment thread internal/lexer/number.go Outdated
errPos = b.Len()
newError := func(code NumberErrorCode, b *strings.Builder) {
if errorType == 0 {
errorType = code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

replace both of the if-statements in newError with if err == nil { err = &NumberError{...} }. Take errPos as a parameter.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Updated newError to use the single assignment logic.

Comment thread pkg/klon/lexer.go Outdated
if first == '-' || first == '+' {
prefix = string(first)
r, n, err := rd.currRune()
if err != nil || (!lexer.IsDigit(r) && r != '.') {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

A number must directly follow the - sign, as descibed in readToken. Leading decimal points aren't allowed (no .5, -.5). if there is a trailing decimal (5.), ReadNumber should already return just 5, and on the Klon part, since . is not punctuation or whitespace, it should start reading an unquoted string.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Removed leading decimal point support in Klon's readToken and readNumber now correctly transitions for trailing decimals.

Comment thread pkg/klon/lexer.go Outdated
num, params := lexer.ReadNumber(rd, first)

// Klon transitions to unquoted string for certain numeric patterns
if strings.Contains(num, "__") || num[0] == '.' || num[len(num)-1] == '_' {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

remove strings.Contains(num, "__"). that should be reported as an error. keep num[len(num)-1] == '_'. keep num[0] == '.' unless it is already handled. remove the following branch in klon's readToken for the '.' rune (keep it for '+'):

if curr, _, _ := rd.currRune(); curr >= '0' && curr <= '9' {
				return rd.readNumber(r, start, bufPos)
			}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Removed __ check, kept trailing underscore/leading dot transition, and updated Klon's readToken to remove the '.' branch.

Comment thread pkg/klon/lexer.go
}

r, _, err := rd.currRune()
isDelim := err != nil || unicode.IsSpace(r) || rd.isPunct(r) || r == ','

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Remove the r == ',' condition.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Removed the comma check from the delimiter condition.

Comment thread pkg/klon/lexer.go
Src: prefix + num,
Pos: start,
BufPos: bufPos,
Attrs: attrs{"params": params},

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reverse these so the number token isn't nested

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed. Reversed the variable names to num, params.

- Refactor `newError` to use `errPos` and modify a nil pointer `NumberError` directly.
- Remove support for leading decimal points in Klon's `readToken`.
- Refine Klon's `readNumber` to correctly transition for trailing dots/underscores.
- Cleanup delimiter checks and variable naming in Klon lexer.
- Remove redundant pattern checks in Klon.

Co-authored-by: ProCode-Software <68168458+ProCode-Software@users.noreply.github.com>
@ProCode-Software
ProCode-Software merged commit d71884d into main May 26, 2026
@ProCode-Software
ProCode-Software deleted the unify-number-lexer-697235292979761754 branch June 29, 2026 07:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant