lexer

A generic lexer library for handwritten lexers in MoonBit

lexer
programming language
moon add bobzhang/lexer@0.2.0
Download zip
Author
Version
0.2.0
License
Apache-2.0
Last updated
2 months ago
Downloads
103K
README

#bobzhang/lexer

A generic lexer library for building handwritten lexers in MoonBit. This library provides essential lexing utilities including position tracking, character advancement, whitespace handling, and error reporting.

#Features

  • Position Tracking: Accurate line and column tracking for better error reporting
  • Unicode Support: Proper handling of surrogate pairs and multi-byte characters
  • Flexible Character Handling: Peek, advance, and expect specific characters or strings
  • Whitespace Management: Skip whitespace while preserving significant newlines
  • String View Integration: Efficient string processing using MoonBit's string views
  • Error Reporting: Detailed error messages with position information

#Installation

moon add bobzhang/lexer

#Types

#Position

The Position struct tracks location information in the input:

///|
#valtype
pub(all) struct Position {
line : Int
column : Int
} derive(Eq, Debug)

#Lexer

The main lexer struct maintains state and position:

  • input: String - The input text being lexed
  • position: Int - Current byte position in input
  • line: Int - Current line number (1-based)
  • column: Int - Current column position (1-based)

#Quick Start

///|
test "quick start example" {
let lexer = @lexer.Lexer::Lexer("key = value")

// Peek at current character without advancing
match lexer.peek() {
Some('k') => inspect("Found 'k'", content="Found 'k'")
_ => ()
}

// Advance through characters
lexer.advance() // moves to 'e'
lexer.advance() // moves to 'y'
lexer.advance() // moves to ' '

// Skip whitespace
lexer.skip_whitespace() // skips spaces, tabs, carriage returns

// Expect specific characters or strings
lexer.expect_char('=') // advances if '=' is found, otherwise raises error
lexer.skip_whitespace()
lexer.expect_string("value") // expects and consumes "value"

// Get current position for error reporting
let pos = lexer.get_loc() // returns Position { line: 1, column: 12 }
inspect(pos.line, content="1")
inspect(pos.column, content="12")
}

#Basic Usage

#Creating a Lexer

///|
test "creating a lexer" {
let lexer = @lexer.Lexer::Lexer("key = value")
inspect(lexer.get_position(), content="0")
inspect(lexer.get_loc().line, content="1")
inspect(lexer.get_loc().column, content="1")
}

#Position Tracking

///|
test "position tracking example" {
let lexer = @lexer.Lexer::Lexer("hello\nworld")
inspect(lexer.get_loc().line, content="1")
inspect(lexer.get_loc().column, content="1")

// Advance through characters
lexer.advance() // h
inspect(lexer.get_loc().column, content="2")

// Handle newlines explicitly
lexer.advance() // move past \n
lexer.new_line() // update line tracking
inspect(lexer.get_loc().line, content="2")
inspect(lexer.get_loc().column, content="1")
}

#API Reference

#Methods

#Creation

  • Lexer::Lexer(input : String) -> Lexer - Create a new lexer with the given input

#Character Operations

  • peek() -> Char? - Get current character without advancing
  • advance() -> Unit - Advance to next character (handles Unicode properly)
  • expect_char(ch : Char, msg? : String) -> Unit raise - Expect and consume a specific character

#String Operations

  • expect_string(str : String, msg? : String) -> Unit raise - Expect and consume a specific string
  • view() -> StringView - Get a view of the remaining input
  • update_view(view : StringView) -> Unit - Update position based on a new view

#Whitespace Handling

  • skip_whitespace() -> Unit - Skip spaces, tabs, and carriage returns (not newlines)
  • skip_single_newline() -> Unit - Skip a single newline and update line tracking

#Position Tracking

  • get_loc() -> Position - Get current line and column position
  • get_position() -> Int - Get current byte position in input
  • new_line() -> Unit - Explicitly advance to new line (updates line/column tracking)

#Error Handling

  • error(msg : String) -> String - Create detailed error message with position info

#Core Methods

///|
test "core methods example" {
let lexer = @lexer.Lexer::Lexer("hello\nworld")
inspect(lexer.get_loc().line, content="1")
inspect(lexer.get_loc().column, content="1")

// Advance through characters
lexer.advance() // h
inspect(lexer.get_loc().column, content="2")

// Handle newlines explicitly
lexer.advance() // move past \n
lexer.new_line() // update line tracking
inspect(lexer.get_loc().line, content="2")
inspect(lexer.get_loc().column, content="1")
}

#Core Methods

#Character Access

///|
test "character access example" {
let lexer = @lexer.Lexer::Lexer("Hello")
debug_inspect(lexer.peek(), content="Some('H')")
lexer.advance()
debug_inspect(lexer.peek(), content="Some('e')")
}

#String Views

The lexer integrates with MoonBit's string views for efficient processing:

///|
test "string views example" {
let lexer = @lexer.Lexer::Lexer("😈x中world!")
match lexer.view() {
[.. "😈x", .. rest] => lexer.update_view(rest)
_ => ()
}
debug_inspect(lexer.peek(), content="Some('中')")
}

#Whitespace Handling

///|
test "whitespace handling example" {
let lexer = @lexer.Lexer::Lexer(" \t\rHello, world!")
lexer.skip_whitespace()
debug_inspect(lexer.peek(), content="Some('H')")
}

#Expectations and Error Handling

The lexer provides methods to expect specific characters or strings:

///|
test "expectations example" {
let lexer = @lexer.Lexer::Lexer("=true")
// Expect a specific character
lexer.expect_char('=', msg="Expected equals sign")

// Expect a string
lexer.expect_string("true", msg="Expected boolean value")

// Error messages include precise position information
let error_msg = lexer.error("Unexpected character")
inspect(error_msg, content="Unexpected character at line 1, column 6")
}

#Advanced Usage

#Unicode Support

The lexer properly handles Unicode characters including surrogate pairs:

///|
test "unicode support example" {
let lexer = @lexer.Lexer::Lexer("😈x")
inspect(lexer.get_position(), content="0")
lexer.advance() // Correctly advances past the 2-byte emoji
inspect(lexer.get_position(), content="2") // 2 bytes for emoji
debug_inspect(lexer.peek(), content="Some('x')")
}

#Position Management

Get current position information:

///|
test "position management example" {
let lexer = @lexer.Lexer::Lexer("test")
let pos = lexer.get_loc() // Get Position struct
let offset = lexer.get_position() // Get byte offset
inspect(pos.line, content="1")
inspect(pos.column, content="1")
inspect(offset, content="0")
}

#Position-Aware Error Handling

///|
test "position aware error handling example" {
let lexer = @lexer.Lexer::Lexer("abc")
lexer.advance() // move to 'b'
lexer.advance() // move to 'c'
let start_pos = lexer.get_loc()
let msg = "Invalid identifier at line " + start_pos.line.to_string()
inspect(msg, content="Invalid identifier at line 1")
}

#Custom Lexer Implementation

///|
test "custom lexer implementation example" {
let lexer = @lexer.Lexer::Lexer("a=b")
let tokens = []

// Simple tokenization - process each character
while lexer.peek() is Some(_) {
lexer.skip_whitespace()
match lexer.peek() {
None => ()
Some('=') => {
lexer.advance()
tokens.push("EQUALS")
}
Some(c) if c.is_ascii_alphabetic() => {
let mut identifier = ""
while lexer.peek() is Some(ch) && ch.is_ascii_alphabetic() {
identifier = identifier + Char::to_string(ch)
lexer.advance()
}
tokens.push("ID:" + identifier)
}
Some(_) => lexer.advance() // skip unknown characters
}
}
debug_inspect(tokens, content="[\"ID:a\", \"EQUALS\", \"ID:b\"]")
}

#Example: Basic Parsing

///|
test "basic parsing example" {
let lexer = @lexer.Lexer::Lexer("key = \"value\"")

// Skip initial whitespace
lexer.skip_whitespace()

// Parse key name
let mut key = ""
while lexer.peek() is Some(ch) && ch != ' ' {
key = key + Char::to_string(ch)
lexer.advance()
}

// Skip whitespace and expect equals
lexer.skip_whitespace()
lexer.expect_char('=')
lexer.skip_whitespace()

// Expect quoted string
lexer.expect_char('"')
let mut value = ""
while lexer.peek() is Some(ch) && ch != '"' {
value = value + Char::to_string(ch)
lexer.advance()
}
lexer.expect_char('"')
inspect(key, content="key")
inspect(value, content="value")
}

#License

Apache-2.0

#
Lexer

pub struct Lexer {
// private fields
}
fn Lexer::Lexer(input : String) -> Lexer

Lexer state with position tracking for better error reporting

#
Lexer::advance

fn Lexer::advance(self : Lexer) -> Unit

Get current character and advance position handle surrogate pairs and multi-byte characters Note: Does not automatically track newlines - call new_line() explicitly when needed

#
Lexer::error

fn Lexer::error(self : Lexer, msg : String) -> String

Create a detailed error message with position information

#
Lexer::expect_char

fn Lexer::expect_char(self : Lexer, ch : Char, msg? : String) -> Unit raise

Expect a specific character and advance, or fail with detailed error

#
Lexer::expect_string

fn Lexer::expect_string(self : Lexer, str : String, msg? : String) -> Unit raise

Expect a string and advance, or fail with detailed error Note the parameter str is not expected to have a newline otherwise the line position is not correct

Example

#
Lexer::get_loc

fn Lexer::get_loc(self : Lexer) -> Position

Return the current 1-based (line, column) position.

Use this to capture the start of a lexeme before consuming it; pair the captured position with the post-consumption value to build the half-open span attached to the resulting token.

#
Lexer::get_position

fn Lexer::get_position(self : Lexer) -> Int

Get current position for error reporting

#
Lexer::new_line

fn Lexer::new_line(self : Lexer) -> Unit

Explicitly advance to a new line (call when encountering '\n') This updates line and column tracking appropriately

#
Lexer::peek

fn Lexer::peek(self : Lexer) -> Char?

Get current character without advancing

Example:

let lexer = Lexer::Lexer("Hello, world!") inspect(lexer.peek(), content="Some('H')") lexer.advance() inspect(lexer.peek(), content="Some('e')")

#
Lexer::peek_charcode

fn Lexer::peek_charcode(self : Lexer) -> UInt16?

Return the raw UTF-16 code unit at the cursor without advancing.

Unlike peek, which decodes a full Char (and so may span a surrogate pair), this returns the underlying 16-bit unit directly. Useful in hot paths where the lexer only needs to test ASCII bytes and wants to skip the UTF-decoding cost. Returns None at end of input.

#
Lexer::skip_single_newline

fn Lexer::skip_single_newline(self : Lexer) -> Unit

Consume one line terminator if the cursor is sitting on one.

Both \n and \r\n are treated as a single newline, and the line counter is advanced. If the next character is not a newline this is a no-op, which makes the call safe to use after constructs that may or may not have left a trailing newline behind (e.g. comments at EOF).

#
Lexer::skip_whitespace

fn Lexer::skip_whitespace(self : Lexer) -> Unit

Skip whitespace characters not including newlines Note: This method does not skip '\n' characters as those are significant in TOML

Example:

let lexer = Lexer::Lexer(" \t\rHello, world!") lexer.skip_whitespace() inspect(lexer.peek(), content="Some('H')")

#
Lexer::update_view

fn Lexer::update_view(self : Lexer, view : StringView) -> Unit

Update the lexer's position and column based on a new view

Example:

let lexer = Lexer::Lexer("😈x中world!") match lexer.view() { [.."😈x", .. rest] => lexer.update_view(rest) _ => () } inspect(lexer.peek(), content="Some('中')")

#
Lexer::view

fn Lexer::view(self : Lexer) -> StringView

Get a view of the input string

Example:

let lexer = Lexer::Lexer("Hello, world!") lexer.advance() lexer.advance() inspect(lexer.view(), content="llo, world!")

#
Position

pub(all) struct Position {
line : Int
column : Int
} derive(Eq,
Debug
)

Lexer implementation for TOML

Source Files

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io