///|
let eof : Int32 = -1
// ============================================================================
// Constructor
// ============================================================================
///|
pub fn WastLexer::new(buffer : Bytes, filename : String) -> WastLexer {
{
buffer,
filename,
line: 1,
cursor: 0,
line_start: 0,
token_start: 0,
errors: [],
}
}
// ============================================================================
// Character Classification
// ============================================================================
///|
fn is_digit(c : Int32) -> Bool {
c >= 0x30 && c <= 0x39 // '0' to '9'
}
///|
fn is_hex_digit(c : Int32) -> Bool {
is_digit(c) ||
(c >= 0x41 && c <= 0x46) || // 'A' to 'F'
(c >= 0x61 && c <= 0x66) // 'a' to 'f'
}
///|
fn is_keyword(c : Int32) -> Bool {
c >= 0x61 && c <= 0x7A // 'a' to 'z'
}
///|
fn is_id_char(c : Int32) -> Bool {
if c < 0x21 || c > 0x7E {
return false
}
// Exclude: " ( ) , ; [ ] { }
match c {
0x22 | 0x28 | 0x29 | 0x2C | 0x3B | 0x5B | 0x5D | 0x7B | 0x7D => false
_ => true
}
}
///|
fn parse_hex_digit(c : Int32) -> Int32 {
if c >= 0x30 && c <= 0x39 {
c - 0x30
} else if c >= 0x41 && c <= 0x46 {
c - 0x41 + 10
} else if c >= 0x61 && c <= 0x66 {
c - 0x61 + 10
} else {
0
}
}
// ============================================================================
// Low-level Character Operations
// ============================================================================
///|
fn WastLexer::peek_char(self : WastLexer) -> Int32 {
if self.cursor < self.buffer.length() {
self.buffer[self.cursor].to_int()
} else {
eof
}
}
///|
fn WastLexer::read_char(self : WastLexer) -> Int32 {
if self.cursor < self.buffer.length() {
let c = self.buffer[self.cursor].to_int()
self.cursor += 1
c
} else {
eof
}
}
///|
fn WastLexer::match_char(self : WastLexer, c : Int32) -> Bool {
if self.peek_char() == c {
let _ = self.read_char()
true
} else {
false
}
}
///|
fn WastLexer::match_string(self : WastLexer, s : String) -> Bool {
let saved_cursor = self.cursor
for i = 0; i < s.length(); i = i + 1 {
if self.read_char() != s[i].to_int() {
self.cursor = saved_cursor
return false
}
}
true
}
///|
fn WastLexer::newline(self : WastLexer) -> Unit {
self.line += 1
self.line_start = self.cursor
}
// ============================================================================
// Token Creation Helpers
// ============================================================================
///|
fn WastLexer::get_location(self : WastLexer) -> Location {
let first_col = if self.token_start >= self.line_start {
self.token_start - self.line_start + 1
} else {
1
}
let last_col = if self.cursor >= self.line_start {
self.cursor - self.line_start + 1
} else {
1
}
{
filename: self.filename,
line: self.line,
first_column: first_col,
last_column: last_col,
}
}
///|
fn WastLexer::get_text(self : WastLexer, offset : Int32) -> String {
let start = self.token_start + offset
if start >= self.buffer.length() || self.cursor <= start {
return ""
}
@utf8.decode(self.buffer[start:self.cursor]) catch {
_ => {
self.error("invalid UTF-8")
""
}
}
}
///|
fn WastLexer::bare_token(self : WastLexer, token_type : TokenType) -> Token {
{ location: self.get_location(), token_type, value: None, }
}
///|
fn WastLexer::literal_token(
self : WastLexer,
token_type : TokenType,
literal_type : LiteralType,
) -> Token {
let text = self.get_text(0)
{
location: self.get_location(),
token_type,
value: Literal({ literal_type, text, }),
}
}
///|
fn WastLexer::text_token(
self : WastLexer,
token_type : TokenType,
offset : Int32,
) -> Token {
{
location: self.get_location(),
token_type,
value: Text(self.get_text(offset)),
}
}
///|
fn WastLexer::error(self : WastLexer, message : String) -> Unit {
self.errors.push({ level: Error, location: self.get_location(), message, })
}
// ============================================================================
// Comment and Whitespace Handling
// ============================================================================
///|
fn WastLexer::read_block_comment(self : WastLexer) -> Bool {
let mut nesting = 1
while true {
match self.read_char() {
c if c == eof => {
self.error("EOF in block comment")
return false
}
0x3B => // ';'
if self.match_char(0x29) {
// ')'
nesting -= 1
if nesting == 0 {
return true
}
}
0x28 => // '('
if self.match_char(0x3B) {
// ';'
nesting += 1
}
0x0A => // '\n'
self.newline()
_ => ()
}
}
false
}
///|
fn WastLexer::read_line_comment(self : WastLexer) -> Bool {
while true {
match self.read_char() {
c if c == eof => return false
0x0D => {
// '\r'
if self.peek_char() == 0x0A {
let _ = self.read_char()
}
self.newline()
return true
}
0x0A => {
// '\n'
self.newline()
return true
}
_ => ()
}
}
false
}
///|
fn WastLexer::read_whitespace(self : WastLexer) -> Unit {
while true {
match self.peek_char() {
0x20 | 0x09 | 0x0D => {
// ' ', '\t', '\r'
let _ = self.read_char()
}
0x0A => {
// '\n'
let _ = self.read_char()
self.newline()
}
_ => return
}
}
}
// ============================================================================
// Number Reading Helpers
// ============================================================================
///|
fn WastLexer::read_num(self : WastLexer) -> Bool {
if !is_digit(self.peek_char()) {
return false
}
while is_digit(self.peek_char()) {
let _ = self.read_char()
if self.match_char(0x5F) && !is_digit(self.peek_char()) {
return false
}
}
true
}
///|
fn WastLexer::read_hex_num(self : WastLexer) -> Bool {
if !is_hex_digit(self.peek_char()) {
return false
}
while is_hex_digit(self.peek_char()) {
let _ = self.read_char()
if self.match_char(0x5F) && !is_hex_digit(self.peek_char()) {
return false
}
}
true
}
///|
fn WastLexer::read_sign(self : WastLexer) -> Unit {
let c = self.peek_char()
if c == 0x2B || c == 0x2D {
// '+' or '-'
let _ = self.read_char()
}
}
///|
fn WastLexer::read_reserved_chars(self : WastLexer) -> ReservedChars {
let mut ret = ReservedChars::None
while true {
let peek = self.peek_char()
if is_id_char(peek) {
let _ = self.read_char()
if ret is ReservedChars::None {
ret = Id
}
} else if peek == 0x22 {
// '"'
let _ = self.get_string_token()
ret = Some
} else {
break
}
}
ret
}
///|
fn WastLexer::no_trailing_reserved_chars(self : WastLexer) -> Bool {
!is_id_char(self.peek_char()) && self.peek_char() != 0x22
}
// ============================================================================
// String Token Parsing
// ============================================================================
///|
fn WastLexer::get_string_token(self : WastLexer) -> Token {
let saved_token_start = self.token_start
let mut has_error = false
let mut in_string = true
let _ = self.read_char() // consume opening '"'
while in_string {
match self.read_char() {
c if c == eof => {
self.token_start = saved_token_start
self.error("EOF in string")
return self.bare_token(Eof)
}
0x0A => {
// '\n'
self.token_start = self.cursor - 1
self.error("newline in string")
has_error = true
self.newline()
continue
}
0x22 => {
// '"'
if self.peek_char() == 0x22 {
self.error("invalid string token")
has_error = true
}
in_string = false
}
0x5C =>
// '\\'
match self.read_char() {
0x74 | 0x6E | 0x72 | 0x22 | 0x27 | 0x5C =>
// 't', 'n', 'r', '"', '\'', '\\'
() // Valid escape
c if is_hex_digit(c) =>
if is_hex_digit(self.peek_char()) {
let _ = self.read_char()
} else {
self.token_start = self.cursor - 2
self.error("bad escape")
has_error = true
}
0x75 => {
// 'u'
self.token_start = self.cursor - 2
if self.read_char() != 0x7B {
// '{'
self.error("bad escape")
has_error = true
continue
}
let mut scalar_value = 0U
let mut has_digit = false
let mut valid_scalar = true
while is_hex_digit(self.peek_char()) {
let digit = parse_hex_digit(self.read_char()).reinterpret_as_uint()
if valid_scalar && scalar_value <= (0x10ffffU - digit) / 16U {
scalar_value = scalar_value * 16U + digit
} else {
valid_scalar = false
}
has_digit = true
if self.match_char(0x5F) && !is_hex_digit(self.peek_char()) {
valid_scalar = false
break
}
}
if !self.match_char(0x7D) {
// '}'
self.error("bad escape")
has_error = true
continue
}
if !has_digit ||
!valid_scalar ||
(scalar_value >= 0xD800U && scalar_value < 0xE000U) {
self.error("bad escape")
has_error = true
}
}
_ => {
self.token_start = self.cursor - 2
self.error("bad escape")
has_error = true
}
}
_ => ()
}
}
self.token_start = saved_token_start
if has_error {
return { location: self.get_location(), token_type: Invalid, value: None, }
}
self.text_token(Text, 0)
}
// ============================================================================
// Number Token Parsing
// ============================================================================
///|
fn WastLexer::get_number_token(
self : WastLexer,
token_type : TokenType,
) -> Token {
let mut tt = token_type
if self.read_num() {
if self.match_char(0x2E) {
// '.'
tt = Float
if is_digit(self.peek_char()) && !self.read_num() {
return self.get_reserved_token()
}
}
if self.match_char(0x65) || self.match_char(0x45) {
// 'e' or 'E'
tt = Float
self.read_sign()
if !self.read_num() {
return self.get_reserved_token()
}
}
if self.no_trailing_reserved_chars() {
if tt is Float {
return self.literal_token(tt, LiteralType::Float)
} else {
return self.literal_token(tt, LiteralType::Int)
}
}
}
self.get_reserved_token()
}
///|
fn WastLexer::get_hex_number_token(
self : WastLexer,
token_type : TokenType,
) -> Token {
let mut tt = token_type
if self.read_hex_num() {
if self.match_char(0x2E) {
// '.'
tt = Float
if is_hex_digit(self.peek_char()) && !self.read_hex_num() {
return self.get_reserved_token()
}
}
if self.match_char(0x70) || self.match_char(0x50) {
// 'p' or 'P'
tt = Float
self.read_sign()
if !self.read_num() {
return self.get_reserved_token()
}
}
if self.no_trailing_reserved_chars() {
if tt is Float {
return self.literal_token(tt, Hexfloat)
} else {
return self.literal_token(tt, LiteralType::Int)
}
}
}
self.get_reserved_token()
}
///|
fn WastLexer::get_inf_token(self : WastLexer) -> Token {
if self.match_string("inf") {
if self.no_trailing_reserved_chars() {
return self.literal_token(Float, Infinity)
}
return self.get_reserved_token()
}
self.get_keyword_token()
}
///|
fn WastLexer::get_nan_token(self : WastLexer) -> Token {
if self.match_string("nan") {
if self.match_char(0x3A) {
// ':'
if self.match_string("0x") {
ignore(self.read_reserved_chars())
if self.no_trailing_reserved_chars() {
return self.literal_token(Float, Nan)
}
} else {
ignore(self.read_reserved_chars())
if self.no_trailing_reserved_chars() {
return self.literal_token(Float, Nan)
}
}
} else if self.no_trailing_reserved_chars() {
return self.literal_token(Float, Nan)
}
}
self.get_keyword_token()
}
///|
fn WastLexer::get_name_eq_num_token(
self : WastLexer,
name : String,
token_type : TokenType,
) -> Token {
if self.match_string(name) {
if self.match_string("0x") {
if self.read_hex_num() && self.no_trailing_reserved_chars() {
return self.text_token(token_type, name.length())
}
} else if self.read_num() && self.no_trailing_reserved_chars() {
return self.text_token(token_type, name.length())
}
}
self.get_keyword_token()
}
// ============================================================================
// Identifier and Keyword Tokens
// ============================================================================
///|
fn WastLexer::get_id_chars(self : WastLexer) -> Token {
if self.read_reserved_chars() is Id {
return self.text_token(Var, 0)
}
self.text_token(Reserved, 0)
}
///|
fn WastLexer::get_keyword_token(self : WastLexer) -> Token {
let _ = self.read_reserved_chars()
let text = self.get_text(0)
match lookup_keyword(text) {
Some(token_type) =>
match token_type {
ValueType(vt) =>
{
location: self.get_location(),
token_type,
value: TokenValue::ValueType(vt),
}
Opcode(op) =>
{
location: self.get_location(),
token_type,
value: TokenValue::Opcode(op),
}
_ => self.bare_token(token_type)
}
None => self.text_token(Reserved, 0)
}
}
///|
fn WastLexer::get_reserved_token(self : WastLexer) -> Token {
let _ = self.read_reserved_chars()
self.text_token(Reserved, 0)
}
// ============================================================================
// Main Tokenization Entry Point
// ============================================================================
///|
pub fn WastLexer::get_token(self : WastLexer) -> Token {
while true {
self.token_start = self.cursor
match self.peek_char() {
c if c == eof => return self.bare_token(Eof)
0x28 =>
// '('
if self.match_string("(;") {
if self.read_block_comment() {
continue
}
return self.bare_token(Eof)
} else if self.match_string("(@") {
let _ = self.get_id_chars()
return self.text_token(LparAnn, 2)
} else {
let _ = self.read_char()
return self.bare_token(Lpar)
}
0x29 => {
// ')'
let _ = self.read_char()
return self.bare_token(Rpar)
}
0x3B =>
// ';'
if self.match_string(";;") {
if self.read_line_comment() {
continue
}
return self.bare_token(Eof)
} else {
let _ = self.read_char()
self.error("unexpected char")
continue
}
0x20 | 0x09 | 0x0D | 0x0A => {
// ' ', '\t', '\r', '\n'
self.read_whitespace()
continue
}
0x22 =>
// '"'
return self.get_string_token()
0x2B | 0x2D => {
// '+', '-'
let _ = self.read_char()
match self.peek_char() {
0x69 =>
// 'i'
return self.get_inf_token()
0x6E =>
// 'n'
return self.get_nan_token()
0x30 =>
// '0'
return if self.match_string("0x") {
self.get_hex_number_token(Int)
} else {
self.get_number_token(Int)
}
c if c >= 0x31 && c <= 0x39 =>
// '1'-'9'
return self.get_number_token(Int)
_ => return self.get_reserved_token()
}
}
0x30 =>
// '0'
return if self.match_string("0x") {
self.get_hex_number_token(Nat)
} else {
self.get_number_token(Nat)
}
c if c >= 0x31 && c <= 0x39 =>
// '1'-'9'
return self.get_number_token(Nat)
0x24 =>
// '$'
return self.get_id_chars()
0x61 =>
// 'a'
return self.get_name_eq_num_token("align=", AlignEqNat)
0x69 =>
// 'i'
return self.get_inf_token()
0x6E =>
// 'n'
return self.get_nan_token()
0x6F =>
// 'o'
return self.get_name_eq_num_token("offset=", OffsetEqNat)
c =>
if is_keyword(c) {
return self.get_keyword_token()
} else if is_id_char(c) {
return self.get_reserved_token()
} else {
let _ = self.read_char()
self.error("unexpected char")
continue
}
}
}
self.bare_token(Eof) // unreachable
}
// ============================================================================
// Utility Functions
// ============================================================================
///|
pub fn WastLexer::get_errors(self : WastLexer) -> Array[LexerError] {
self.errors
}
///|
pub fn WastLexer::has_errors(self : WastLexer) -> Bool {
self.errors.length() > 0
}
// ============================================================================
// lexer_test.mbt - Comprehensive Tests
// ============================================================================
///|
fn string_to_bytes(s : String) -> Bytes {
let bytes = @buffer.new(size_hint=s.length())
for i = 0; i < s.length(); i = i + 1 {
bytes.write_byte(s[i].to_int().to_byte())
}
bytes.to_bytes()
}
///|
fn make_lexer(source : String) -> WastLexer {
WastLexer::new(string_to_bytes(source), "test.wast")
}
///|
fn collect_tokens(lexer : WastLexer) -> Array[Token] {
let tokens : Array[Token] = []
while true {
let token = lexer.get_token()
tokens.push(token)
if token.token_type is Eof {
break
}
}
tokens
}
///|
priv suberror TokenTypeMismatch
///|
priv suberror ExpectedLiteral
///|
priv suberror ExpectedIntLiteral
///|
priv suberror ExpectedFloatLiteral
///|
priv suberror ExpectedHexFloatLiteral
///|
priv suberror ExpectedInfinityLiteral
///|
priv suberror ExpectedNanLiteral
///|
priv suberror ExpectedText
///|
fn assert_token_type(token : Token, expected : TokenType) -> Unit raise Error {
// Simple comparison - in real code you'd implement proper equality
let matches = match (token.token_type, expected) {
(Eof, Eof) => true
(Lpar, Lpar) => true
(Rpar, Rpar) => true
(LparAnn, LparAnn) => true
(Nat, Nat) => true
(Int, Int) => true
(Float, Float) => true
(Text, Text) => true
(Var, Var) => true
(Reserved, Reserved) => true
(AlignEqNat, AlignEqNat) => true
(OffsetEqNat, OffsetEqNat) => true
(Invalid, Invalid) => true
(Module, Module) => true
(Func, Func) => true
(Param, Param) => true
(Result, Result) => true
(Local, Local) => true
(Global, Global) => true
(Type, Type) => true
(Table, Table) => true
(Memory, Memory) => true
(Import, Import) => true
(Export, Export) => true
(Block, Block) => true
(Loop, Loop) => true
(If, If) => true
(Else, Else) => true
(End, End) => true
(ValueType(_), ValueType(_)) => true
(Opcode(_), Opcode(_)) => true
_ => false
}
if !matches {
raise TokenTypeMismatch
}
}
// ============================================================================
// Basic Token Tests
// ============================================================================
///|
test "empty input returns EOF" {
let lexer = make_lexer("")
let token = lexer.get_token()
assert_token_type(token, Eof)
}
///|
test "whitespace only returns EOF" {
let lexer = make_lexer(" \t\n\r\n ")
let token = lexer.get_token()
assert_token_type(token, Eof)
}
///|
test "left paren" {
let lexer = make_lexer("(")
let token = lexer.get_token()
assert_token_type(token, Lpar)
}
///|
test "right paren" {
let lexer = make_lexer(")")
let token = lexer.get_token()
assert_token_type(token, Rpar)
}
///|
test "multiple parens" {
let lexer = make_lexer("(())")
let tokens = collect_tokens(lexer)
@debug.assert_eq(tokens.length(), 5) // ( ( ) ) EOF
assert_token_type(tokens[0], Lpar)
assert_token_type(tokens[1], Lpar)
assert_token_type(tokens[2], Rpar)
assert_token_type(tokens[3], Rpar)
assert_token_type(tokens[4], Eof)
}
// ============================================================================
// Comment Tests
// ============================================================================
///|
test "line comment" {
let lexer = make_lexer(";; this is a comment\n(")
let token = lexer.get_token()
assert_token_type(token, Lpar)
}
///|
test "line comment at EOF" {
let lexer = make_lexer(";; comment without newline")
let token = lexer.get_token()
assert_token_type(token, Eof)
}
///|
test "block comment" {
let lexer = make_lexer("(; block comment ;)(")
let token = lexer.get_token()
assert_token_type(token, Lpar)
}
///|
test "nested block comment" {
let lexer = make_lexer("(; outer (; inner ;) outer ;)(")
let token = lexer.get_token()
assert_token_type(token, Lpar)
}
///|
test "block comment with newlines" {
let lexer = make_lexer("(; line 1\nline 2\nline 3 ;)(")
let token = lexer.get_token()
assert_token_type(token, Lpar)
@debug.assert_eq(lexer.line, 3)
}
// ============================================================================
// Number Tests
// ============================================================================
///|
test "natural number" {
let lexer = make_lexer("42")
let token = lexer.get_token()
assert_token_type(token, Nat)
match token.value {
Literal(lit) => @debug.assert_eq(lit.text, "42")
_ => raise ExpectedLiteral
}
}
///|
test "natural number zero" {
let lexer = make_lexer("0")
let token = lexer.get_token()
assert_token_type(token, Nat)
}
///|
test "positive integer" {
let lexer = make_lexer("+123")
let token = lexer.get_token()
assert_token_type(token, Int)
match token.value {
Literal(lit) => @debug.assert_eq(lit.text, "+123")
_ => raise ExpectedLiteral
}
}
///|
test "negative integer" {
let lexer = make_lexer("-456")
let token = lexer.get_token()
assert_token_type(token, Int)
match token.value {
Literal(lit) => @debug.assert_eq(lit.text, "-456")
_ => raise ExpectedLiteral
}
}
///|
test "hex number" {
let lexer = make_lexer("0xDEADBEEF")
let token = lexer.get_token()
assert_token_type(token, Nat)
match token.value {
Literal(lit) => @debug.assert_eq(lit.text, "0xDEADBEEF")
_ => raise ExpectedLiteral
}
}
///|
test "hex number lowercase" {
let lexer = make_lexer("0xabcdef")
let token = lexer.get_token()
assert_token_type(token, Nat)
}
///|
test "number with underscores" {
let lexer = make_lexer("1_000_000")
let token = lexer.get_token()
assert_token_type(token, Nat)
match token.value {
Literal(lit) => @debug.assert_eq(lit.text, "1_000_000")
_ => raise ExpectedLiteral
}
}
///|
test "hex number with underscores" {
let lexer = make_lexer("0xDEAD_BEEF")
let token = lexer.get_token()
assert_token_type(token, Nat)
}
///|
test "float decimal" {
let lexer = make_lexer("3.14")
let token = lexer.get_token()
assert_token_type(token, Float)
match token.value {
Literal(lit) => {
@debug.assert_eq(lit.text, "3.14")
match lit.literal_type {
LiteralType::Float => ()
_ => raise ExpectedFloatLiteral
}
}
_ => raise ExpectedLiteral
}
}
///|
test "float with exponent" {
let lexer = make_lexer("1.5e10")
let token = lexer.get_token()
assert_token_type(token, Float)
}
///|
test "float with negative exponent" {
let lexer = make_lexer("2.5e-3")
let token = lexer.get_token()
assert_token_type(token, Float)
}
///|
test "float with positive exponent" {
let lexer = make_lexer("1E+5")
let token = lexer.get_token()
assert_token_type(token, Float)
}
///|
test "hex float" {
let lexer = make_lexer("0x1.fp10")
let token = lexer.get_token()
assert_token_type(token, Float)
match token.value {
Literal(lit) =>
match lit.literal_type {
Hexfloat => ()
_ => raise ExpectedHexFloatLiteral
}
_ => raise ExpectedLiteral
}
}
///|
test "infinity" {
let lexer = make_lexer("inf")
let token = lexer.get_token()
assert_token_type(token, Float)
match token.value {
Literal(lit) =>
match lit.literal_type {
Infinity => ()
_ => raise ExpectedInfinityLiteral
}
_ => raise ExpectedLiteral
}
}
///|
test "positive infinity" {
let lexer = make_lexer("+inf")
let token = lexer.get_token()
assert_token_type(token, Float)
}
///|
test "negative infinity" {
let lexer = make_lexer("-inf")
let token = lexer.get_token()
assert_token_type(token, Float)
}
///|
test "nan" {
let lexer = make_lexer("nan")
let token = lexer.get_token()
assert_token_type(token, Float)
match token.value {
Literal(lit) =>
match lit.literal_type {
Nan => ()
_ => raise ExpectedNanLiteral
}
_ => raise ExpectedLiteral
}
}
///|
test "nan with payload" {
let lexer = make_lexer("nan:0x7fc00000")
let token = lexer.get_token()
assert_token_type(token, Float)
}
// ============================================================================
// String Tests
// ============================================================================
///|
test "simple string" {
let lexer = make_lexer("\"hello\"")
let token = lexer.get_token()
assert_token_type(token, Text)
match token.value {
TokenValue::Text(s) => @debug.assert_eq(s, "\"hello\"")
_ => raise ExpectedText
}
}
///|
test "empty string" {
let lexer = make_lexer("\"\"")
let token = lexer.get_token()
assert_token_type(token, Text)
}
///|
test "string with escape sequences" {
let lexer = make_lexer("\"hello\\nworld\"")
let token = lexer.get_token()
assert_token_type(token, Text)
}
///|
test "string with tab escape" {
let lexer = make_lexer("\"col1\\tcol2\"")
let token = lexer.get_token()
assert_token_type(token, Text)
}
///|
test "string with hex escape" {
let lexer = make_lexer("\"\\41\\42\\43\"")
let token = lexer.get_token()
assert_token_type(token, Text)
}
///|
test "string with unicode escape" {
let lexer = make_lexer("\"\\u{0041}\"")
let token = lexer.get_token()
assert_token_type(token, Text)
}
///|
test "string with quote escape" {
let lexer = make_lexer("\"say \\\"hello\\\"\"")
let token = lexer.get_token()
assert_token_type(token, Text)
}
///|
test "string with backslash escape" {
let lexer = make_lexer("\"path\\\\to\\\\file\"")
let token = lexer.get_token()
assert_token_type(token, Text)
}
// ============================================================================
// Variable Tests
// ============================================================================
///|
test "variable with dollar" {
let lexer = make_lexer("$foo")
let token = lexer.get_token()
assert_token_type(token, Var)
match token.value {
TokenValue::Text(s) => @debug.assert_eq(s, "$foo")
_ => raise ExpectedText
}
}
///|
test "variable with numbers" {
let lexer = make_lexer("$var123")
let token = lexer.get_token()
assert_token_type(token, Var)
}
///|
test "variable with underscores" {
let lexer = make_lexer("$my_var_name")
let token = lexer.get_token()
assert_token_type(token, Var)
}
///|
test "variable with special chars" {
let lexer = make_lexer("$<>+-*/")
let token = lexer.get_token()
assert_token_type(token, Var)
}
// ============================================================================
// Keyword Tests
// ============================================================================
///|
test "module keyword" {
let lexer = make_lexer("module")
let token = lexer.get_token()
assert_token_type(token, Module)
}
///|
test "func keyword" {
let lexer = make_lexer("func")
let token = lexer.get_token()
assert_token_type(token, Func)
}
///|
test "param keyword" {
let lexer = make_lexer("param")
let token = lexer.get_token()
assert_token_type(token, Param)
}
///|
test "result keyword" {
let lexer = make_lexer("result")
let token = lexer.get_token()
assert_token_type(token, Result)
}
///|
test "local keyword" {
let lexer = make_lexer("local")
let token = lexer.get_token()
assert_token_type(token, Local)
}
///|
test "global keyword" {
let lexer = make_lexer("global")
let token = lexer.get_token()
assert_token_type(token, Global)
}
///|
test "type keyword" {
let lexer = make_lexer("type")
let token = lexer.get_token()
assert_token_type(token, Type)
}
///|
test "import keyword" {
let lexer = make_lexer("import")
let token = lexer.get_token()
assert_token_type(token, Import)
}
///|
test "export keyword" {
let lexer = make_lexer("export")
let token = lexer.get_token()
assert_token_type(token, Export)
}
///|
test "memory keyword" {
let lexer = make_lexer("memory")
let token = lexer.get_token()
assert_token_type(token, Memory)
}
///|
test "table keyword" {
let lexer = make_lexer("table")
let token = lexer.get_token()
assert_token_type(token, Table)
}
// ============================================================================
// Type Keyword Tests
// ============================================================================
///|
test "i32 type" {
let lexer = make_lexer("i32")
let token = lexer.get_token()
assert_token_type(token, ValueType(I32))
}
///|
test "i64 type" {
let lexer = make_lexer("i64")
let token = lexer.get_token()
assert_token_type(token, ValueType(I64))
}
///|
test "f32 type" {
let lexer = make_lexer("f32")
let token = lexer.get_token()
assert_token_type(token, ValueType(F32))
}
///|
test "f64 type" {
let lexer = make_lexer("f64")
let token = lexer.get_token()
assert_token_type(token, ValueType(F64))
}
///|
test "funcref type" {
let lexer = make_lexer("funcref")
let token = lexer.get_token()
assert_token_type(token, ValueType(FuncRef))
}
///|
test "externref type" {
let lexer = make_lexer("externref")
let token = lexer.get_token()
assert_token_type(token, ValueType(ExternRef))
}
// ============================================================================
// Instruction Tests
// ============================================================================
///|
test "i32.add instruction" {
let lexer = make_lexer("i32.add")
let token = lexer.get_token()
assert_token_type(token, Opcode(I32Add))
}
///|
test "i32.sub instruction" {
let lexer = make_lexer("i32.sub")
let token = lexer.get_token()
assert_token_type(token, Opcode(I32Sub))
}
///|
test "i32.const instruction" {
let lexer = make_lexer("i32.const")
let token = lexer.get_token()
assert_token_type(token, Opcode(I32Const))
}
///|
test "local.get instruction" {
let lexer = make_lexer("local.get")
let token = lexer.get_token()
assert_token_type(token, Opcode(LocalGet))
}
///|
test "local.set instruction" {
let lexer = make_lexer("local.set")
let token = lexer.get_token()
assert_token_type(token, Opcode(LocalSet))
}
///|
test "descriptor reference opcodes" {
let lexer = make_lexer(
"ref.get_desc ref.test_desc ref.test_desc_null ref.cast_desc_eq ref.cast_desc_eq_null",
)
assert_token_type(lexer.get_token(), Opcode(RefGetDesc))
assert_token_type(lexer.get_token(), Opcode(RefTestDesc))
assert_token_type(lexer.get_token(), Opcode(RefTestDescNull))
assert_token_type(lexer.get_token(), Opcode(RefCastDescEq))
assert_token_type(lexer.get_token(), Opcode(RefCastDescEqNull))
}
///|
test "struct allocation opcodes" {
let lexer = make_lexer(
"struct.new struct.new_default struct.new_desc struct.new_default_desc",
)
assert_token_type(lexer.get_token(), Opcode(StructNew))
assert_token_type(lexer.get_token(), Opcode(StructNewDefault))
assert_token_type(lexer.get_token(), Opcode(StructNewDesc))
assert_token_type(lexer.get_token(), Opcode(StructNewDefaultDesc))
}
///|
test "struct access opcodes" {
let lexer = make_lexer("struct.get struct.get_s struct.get_u")
assert_token_type(lexer.get_token(), Opcode(StructGet))
assert_token_type(lexer.get_token(), Opcode(StructGetS))
assert_token_type(lexer.get_token(), Opcode(StructGetU))
}
///|
test "advanced reference and exception keyword classification paths" {
let lexer = make_lexer(
"ref.eq ref.as_non_null any.convert_extern extern.convert_any ref.i31 i31.get_s i31.get_u throw throw_ref try_table try do catch catch_ref catch_all catch_all_ref delegate rethrow tag",
)
assert_token_type(lexer.get_token(), Opcode(RefEq))
assert_token_type(lexer.get_token(), Opcode(RefAsNonNull))
assert_token_type(lexer.get_token(), Opcode(AnyConvertExtern))
assert_token_type(lexer.get_token(), Opcode(ExternConvertAny))
assert_token_type(lexer.get_token(), Opcode(RefI31))
assert_token_type(lexer.get_token(), Opcode(I31GetS))
assert_token_type(lexer.get_token(), Opcode(I31GetU))
let throw_token = lexer.get_token()
assert_true(throw_token.token_type is Opcode(_))
let throw_ref_token = lexer.get_token()
assert_true(throw_ref_token.token_type is Opcode(_))
let try_table_token = lexer.get_token()
assert_true(try_table_token.token_type is Opcode(_))
let legacy_try_token = lexer.get_token()
assert_false(legacy_try_token.token_type is Reserved)
let do_token = lexer.get_token()
assert_false(do_token.token_type is Reserved)
let catch_token = lexer.get_token()
assert_false(catch_token.token_type is Reserved)
let catch_ref_token = lexer.get_token()
assert_false(catch_ref_token.token_type is Reserved)
let catch_all_token = lexer.get_token()
assert_false(catch_all_token.token_type is Reserved)
let catch_all_ref_token = lexer.get_token()
assert_false(catch_all_ref_token.token_type is Reserved)
let delegate_token = lexer.get_token()
assert_false(delegate_token.token_type is Reserved)
let rethrow_token = lexer.get_token()
assert_false(rethrow_token.token_type is Reserved)
let tag_token = lexer.get_token()
assert_false(tag_token.token_type is Reserved)
}
///|
test "call instruction" {
let lexer = make_lexer("call")
let token = lexer.get_token()
assert_token_type(token, Opcode(Call))
}
///|
test "drop instruction" {
let lexer = make_lexer("drop")
let token = lexer.get_token()
assert_token_type(token, Opcode(Drop))
}
///|
test "return instruction" {
let lexer = make_lexer("return")
let token = lexer.get_token()
assert_token_type(token, Opcode(Return))
}
///|
test "unreachable instruction" {
let lexer = make_lexer("unreachable")
let token = lexer.get_token()
assert_token_type(token, Opcode(Unreachable))
}
///|
test "nop instruction" {
let lexer = make_lexer("nop")
let token = lexer.get_token()
assert_token_type(token, Opcode(Nop))
}
// ============================================================================
// Control Flow Tests
// ============================================================================
///|
test "block keyword" {
let lexer = make_lexer("block")
let token = lexer.get_token()
assert_token_type(token, Block)
}
///|
test "loop keyword" {
let lexer = make_lexer("loop")
let token = lexer.get_token()
assert_token_type(token, Loop)
}
///|
test "if keyword" {
let lexer = make_lexer("if")
let token = lexer.get_token()
assert_token_type(token, If)
}
///|
test "else keyword" {
let lexer = make_lexer("else")
let token = lexer.get_token()
assert_token_type(token, Else)
}
///|
test "end keyword" {
let lexer = make_lexer("end")
let token = lexer.get_token()
assert_token_type(token, End)
}
///|
test "br instruction" {
let lexer = make_lexer("br")
let token = lexer.get_token()
assert_token_type(token, Opcode(Br))
}
///|
test "br_if instruction" {
let lexer = make_lexer("br_if")
let token = lexer.get_token()
assert_token_type(token, Opcode(BrIf))
}
// ============================================================================
// Annotation Tests
// ============================================================================
///|
test "annotation" {
let lexer = make_lexer("(@custom)")
let token = lexer.get_token()
assert_token_type(token, LparAnn)
match token.value {
TokenValue::Text(s) => @debug.assert_eq(s, "custom")
_ => raise ExpectedText
}
}
// ============================================================================
// Align and Offset Tests
// ============================================================================
///|
test "align equals nat" {
let lexer = make_lexer("align=4")
let token = lexer.get_token()
assert_token_type(token, AlignEqNat)
match token.value {
TokenValue::Text(s) => @debug.assert_eq(s, "4")
_ => raise ExpectedText
}
}
///|
test "align equals hex" {
let lexer = make_lexer("align=0x10")
let token = lexer.get_token()
assert_token_type(token, AlignEqNat)
}
///|
test "offset equals nat" {
let lexer = make_lexer("offset=100")
let token = lexer.get_token()
assert_token_type(token, OffsetEqNat)
match token.value {
TokenValue::Text(s) => @debug.assert_eq(s, "100")
_ => raise ExpectedText
}
}
///|
test "offset equals hex" {
let lexer = make_lexer("offset=0xFF")
let token = lexer.get_token()
assert_token_type(token, OffsetEqNat)
}
// ============================================================================
// Location Tests
// ============================================================================
///|
test "location tracking" {
let lexer = make_lexer("(func)")
let token1 = lexer.get_token() // (
@debug.assert_eq(token1.location.line, 1)
@debug.assert_eq(token1.location.first_column, 1)
let token2 = lexer.get_token() // func
@debug.assert_eq(token2.location.first_column, 2)
}
///|
test "location tracking across newlines" {
let lexer = make_lexer("(\n func\n)")
let _ = lexer.get_token() // (
let token2 = lexer.get_token() // func
@debug.assert_eq(token2.location.line, 2)
let token3 = lexer.get_token() // )
@debug.assert_eq(token3.location.line, 3)
}
// ============================================================================
// Integration Tests - Full Module Parsing
// ============================================================================
///|
test "simple function" {
let source =
#|(module
#| (func $add (param i32 i32) (result i32)
#| local.get 0
#| local.get 1
#| i32.add))
let lexer = make_lexer(source)
let tokens = collect_tokens(lexer)
// Should tokenize without errors
assert_true(tokens.length() > 0)
assert_token_type(tokens[tokens.length() - 1], Eof)
assert_false(lexer.has_errors())
}
///|
test "memory and data" {
let source =
#|(module
#| (memory 1)
#| (data (i32.const 0) "hello"))
let lexer = make_lexer(source)
let _ = collect_tokens(lexer)
assert_false(lexer.has_errors())
}
///|
test "import and export" {
let source =
#|(module
#| (import "env" "log" (func $log (param i32)))
#| (export "main" (func $main))
#| (func $main))
let lexer = make_lexer(source)
let _ = collect_tokens(lexer)
assert_false(lexer.has_errors())
}
///|
test "global variables" {
let source =
#|(module
#| (global $counter (mut i32) (i32.const 0))
#| (global $max i32 (i32.const 100)))
let lexer = make_lexer(source)
let _ = collect_tokens(lexer)
assert_false(lexer.has_errors())
}
///|
test "control flow" {
let source =
#|(module
#| (func $fib (param i32) (result i32)
#| (if (result i32) (i32.le_s (local.get 0) (i32.const 1))
#| (then (local.get 0))
#| (else
#| (i32.add
#| (call $fib (i32.sub (local.get 0) (i32.const 1)))
#| (call $fib (i32.sub (local.get 0) (i32.const 2))))))))
let lexer = make_lexer(source)
let _ = collect_tokens(lexer)
assert_false(lexer.has_errors())
}
// ============================================================================
// Error Recovery Tests
// ============================================================================
///|
test "error on unexpected character" {
let lexer = make_lexer("(func @)")
let _ = collect_tokens(lexer)
// Should have recorded an error but continued
// The @ character should cause an error
}
///|
test "unterminated block comment" {
let lexer = make_lexer("(; unterminated")
let token = lexer.get_token()
// Should return EOF and record error
assert_token_type(token, Eof)
assert_true(lexer.has_errors())
}
// ============================================================================
// Edge Cases
// ============================================================================
///|
test "reserved token" {
let lexer = make_lexer("notakeyword")
let token = lexer.get_token()
assert_token_type(token, Reserved)
}
///|
test "keywords with common prefix" {
let source = "local local.get local.set local.tee"
let lexer = make_lexer(source)
let t1 = lexer.get_token()
assert_token_type(t1, Local)
let t2 = lexer.get_token()
assert_token_type(t2, Opcode(LocalGet))
let t3 = lexer.get_token()
assert_token_type(t3, Opcode(LocalSet))
let t4 = lexer.get_token()
assert_token_type(t4, Opcode(LocalTee))
}
///|
test "float vs int distinction" {
let lexer1 = make_lexer("42")
let t1 = lexer1.get_token()
assert_token_type(t1, Nat)
let lexer2 = make_lexer("42.0")
let t2 = lexer2.get_token()
assert_token_type(t2, Float)
let lexer3 = make_lexer("42e5")
let t3 = lexer3.get_token()
assert_token_type(t3, Float)
}
///|
test "hex vs decimal distinction" {
let lexer1 = make_lexer("255")
let t1 = lexer1.get_token()
match t1.value {
Literal(lit) =>
match lit.literal_type {
LiteralType::Int => ()
_ => raise ExpectedIntLiteral
}
_ => raise ExpectedLiteral
}
let lexer2 = make_lexer("0xFF")
let t2 = lexer2.get_token()
match t2.value {
Literal(lit) =>
match lit.literal_type {
LiteralType::Int => ()
_ => raise ExpectedIntLiteral
}
_ => raise ExpectedLiteral
}
}
///|
test "many tokens stress test" {
let mut source = ""
for i = 0; i < 100; i = i + 1 {
source = source +
"(func $f" +
i.to_string() +
" (param i32) (result i32) local.get 0)\n"
}
let lexer = make_lexer(source)
let tokens = collect_tokens(lexer)
assert_true(tokens.length() > 1000)
assert_false(lexer.has_errors())
}
///|
test "mixed whitespace" {
let lexer = make_lexer("(\t\r\n func \t)")
let tokens = collect_tokens(lexer)
@debug.assert_eq(tokens.length(), 4) // ( func ) EOF
}