Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 9 additions & 5 deletions effekt/jvm/src/test/scala/effekt/LexerTests.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package effekt

import effekt.lexer.TokenKind.*
import effekt.lexer.{Lexer, TokenKind, LexerError }
import effekt.lexer.{Lexer, LexerError, TokenKind}
import effekt.lexer.LexerError.*

import effekt.util.UByte
import kiama.util.StringSource

import munit.Location

class LexerTests extends munit.FunSuite {
Expand Down Expand Up @@ -79,15 +78,20 @@ class LexerTests extends munit.FunSuite {
}

test("numbers") {
val num = "12.34 100 200 123.345 1 -12.34 -100 -123.345 -1"
val num = "12.34 100 200 123.345 1 -12.34 -100 -123.345 -1 0x42"
assertTokensEq(
num,
Float(12.34), Integer(100), Integer(200), Float(123.345), Integer(1),
Float(-12.34), Integer(-100), Float(-123.345), Integer(-1),
Float(-12.34), Integer(-100), Float(-123.345), Integer(-1), Byt(UByte.lit(0x42)),
EOF
)
}

test("illegal byte notation") {
assertFailure("0x4")
assertFailure("0x444")
}

test("big numbers") {
assertFailure("9223372036854775808")
assertTokensEq("9223372036854775807", Integer(9223372036854775807L), EOF)
Expand Down
1 change: 1 addition & 0 deletions effekt/jvm/src/test/scala/effekt/ParserTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ class ParserTests extends munit.FunSuite {

test("Simple expressions") {
parseExpr("42")
parseExpr("0x42")
parseExpr("f")
parseExpr("f(a)")
parseExpr("f(a, 42)")
Expand Down
28 changes: 27 additions & 1 deletion effekt/shared/src/main/scala/effekt/Lexer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package effekt.lexer
import scala.collection.mutable
import scala.collection.immutable
import effekt.source.Span
import effekt.util.UByte
import kiama.util.Source

/** Lexing errors that can occur during tokenization */
Expand All @@ -16,6 +17,7 @@ enum LexerError {
case MultipleCodePointsInChar
case InvalidIntegerFormat
case InvalidDoubleFormat
case InvalidByteFormat
case UnterminatedInterpolation(depth: Int)

def message: String = this match {
Expand All @@ -34,6 +36,7 @@ enum LexerError {
case MultipleCodePointsInChar => "Character literal consists of multiple code points"
case InvalidIntegerFormat => "Invalid integer format, not a 64bit integer literal"
case InvalidDoubleFormat => "Invalid float format, not a double literal"
case InvalidByteFormat => "Invalid byte format, byte has to be exactly two hex digits"
case UnterminatedInterpolation(depth) =>
s"Unterminated string interpolation ($depth unclosed splices)"
}
Expand Down Expand Up @@ -63,6 +66,7 @@ enum TokenKind {
case Str(s: String, multiline: Boolean)
case HoleStr(s: String)
case Chr(c: Int)
case Byt(b: UByte)

// identifiers
case Ident(id: String)
Expand Down Expand Up @@ -381,7 +385,8 @@ class Lexer(source: Source) extends Iterator[Token] {
case (c, _) if c.isWhitespace => advanceSpaces()

// Numbers
case (c, _) if c.isDigit => number()
case ('0', 'x') if isHexDigit(peekAhead(2)) => advance2With(byte())
case (c, _) if c.isDigit => number()

// Identifiers and keywords
case (c, _) if isNameFirst(c) => identifier()
Expand Down Expand Up @@ -512,6 +517,27 @@ class Lexer(source: Source) extends Iterator[Token] {
case None => TokenKind.Error(LexerError.InvalidIntegerFormat)
}

private def byte(): TokenKind =
// Consume hex digits
advanceWhile { (curr, _) => isHexDigit(curr) }

// Get the hex string
val hexString = getCurrentSlice(skipAfterStart = 2)

if hexString.length < 2 then
return TokenKind.Error(LexerError.InvalidByteFormat)

if hexString.length > 2 then
return TokenKind.Error(LexerError.InvalidByteFormat)

try {
val byte = java.lang.Integer.parseInt(hexString, 16)
assert(byte >= 0 && byte <= 255)
TokenKind.Byt(UByte.unsafeFromInt(byte))
} catch {
case e: NumberFormatException => TokenKind.Error(LexerError.InvalidByteFormat)
}

private def identifier(): TokenKind =
advanceWhile { (curr, _) => isNameRest(curr) }
val word = getCurrentSlice()
Expand Down
3 changes: 2 additions & 1 deletion effekt/shared/src/main/scala/effekt/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ class Parser(tokens: Seq[Token], source: Source) {
}

def isLiteral: Boolean = peek.kind match {
case _: (Integer | Float | Str | Chr) => true
case _: (Integer | Float | Str | Chr | Byt) => true
case `true` => true
case `false` => true
case _ => isUnitLiteral
Expand Down Expand Up @@ -1395,6 +1395,7 @@ class Parser(tokens: Seq[Token], source: Source) {
case Float(v) => skip(); DoubleLit(v, span())
case Str(s, multiline) => skip(); StringLit(s, span())
case Chr(c) => skip(); CharLit(c, span())
case Byt(b) => skip(); ByteLit(b, span())
case `true` => skip(); BooleanLit(true, span())
case `false` => skip(); BooleanLit(false, span())
case t if isUnitLiteral => skip(); skip(); UnitLit(span())
Expand Down
10 changes: 9 additions & 1 deletion effekt/shared/src/main/scala/effekt/core/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package core
import effekt.core.Type.{PromptSymbol, ResumeSymbol}
import effekt.source.{FeatureFlag, Span}
import effekt.symbols.builtins
import effekt.util.UByte
import effekt.util.messages.{ErrorReporter, ParseError}
import kiama.parsing.{NoSuccess, ParseResult, Parsers, Success}
import kiama.util.{Position, Range, Severities, Source, StringSource}
Expand Down Expand Up @@ -156,6 +157,12 @@ class EffektLexers extends Parsers {
lazy val unicodeChar = regex("""\\u\{[0-9A-Fa-f]{1,6}\}""".r, "Unicode character literal") ^^ {
case contents => Integer.parseInt(contents.stripPrefix("\\u{").stripSuffix("}"), 16)
}
lazy val byteLiteral = regex("0x([0-9A-F]{2})".r, "Byte literal") ^^ { case s =>
val hexPart = s.substring(2) // strip the 0x prefix
val intVal = Integer.parseInt(hexPart, 16) // parse as unsigned int (0..255)
UByte.unsafeFromInt(intVal)
}


/** Inverse of PrettyPrinter.escapeString */
private def unescapeString(s: String): String = {
Expand Down Expand Up @@ -266,6 +273,7 @@ class CoreParsers(names: Names) extends EffektLexers {
*/
lazy val int = integerLiteral ^^ { n => Literal(n.toLong, Type.TInt) }
lazy val char = charLiteral ^^ { n => Literal(n.toLong, Type.TChar) }
lazy val byte = byteLiteral ^^ { b => Literal(b, Type.TByte) }
lazy val bool = `true` ^^^ Literal(true, Type.TBoolean) | `false` ^^^ Literal(false, Type.TBoolean)
lazy val unit = literal("()") ^^^ Literal((), Type.TUnit)
lazy val double = doubleLiteral ^^ { n => Literal(n.toDouble, Type.TDouble) }
Expand Down Expand Up @@ -443,7 +451,7 @@ class CoreParsers(names: Names) extends EffektLexers {
| failure("Expected a pure expression.")
)

lazy val literal: P[Expr] = double | int | char | bool | string | unit
lazy val literal: P[Expr] = byte | double | int | char | bool | string | unit


// Calls
Expand Down
6 changes: 4 additions & 2 deletions effekt/shared/src/main/scala/effekt/core/PrettyPrinter.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package effekt
package core

import effekt.core.Type.{ PromptSymbol, ResumeSymbol, TChar }
import effekt.core.Type.{PromptSymbol, ResumeSymbol, TChar}
import effekt.source.FeatureFlag
import kiama.output.ParenPrettyPrinter
import kiama.output.PrettyPrinterTypes.Document

import scala.language.implicitConversions
import effekt.symbols.{ Name, Wildcard, builtins }
import effekt.symbols.{Name, Wildcard, builtins}
import effekt.util.UByte

class PrettyPrinter(printDetails: Boolean, printInternalIds: Boolean = true) extends ParenPrettyPrinter {
override val defaultIndent = 2
Expand Down Expand Up @@ -131,6 +132,7 @@ class PrettyPrinter(printDetails: Boolean, printInternalIds: Boolean = true) ext
case Literal((), _) => "()"
case Literal(n, Type.TInt) => n.toString
case Literal(n, Type.TChar) => s"'\\${n.toString}'"
case Literal(b: Byte, Type.TByte) => UByte.unsafeFromByte(b).toHexString
Copy link
Contributor Author

@jiribenes jiribenes Dec 17, 2025

Choose a reason for hiding this comment

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

This might be a bit confusing, but we can't match on a UByte as it's opaque, so we have to match on a Byte, just to zero-cost convert it to a Byte, just to print the hexString consistently.
... I really don't know how to do this better without either wrapping it in a custom type and dealing with the overhead, or pulling a newtype library :/

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's ok. You could have toHexString operate directly on Byte and use toUnsignedInt everywhere.

case Literal(s: String, _) => stringLiteral(s)
case Literal(value, _) => value.toString
case ValueVar(id, tpe) => toDoc(id) <> (if printDetails then ":" <+> toDoc(tpe) else emptyDoc)
Expand Down
26 changes: 14 additions & 12 deletions effekt/shared/src/main/scala/effekt/core/vm/Builtin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package effekt
package core
package vm

import effekt.util.UByte

import java.io.PrintStream
import scala.util.matching as regex
import scala.util.matching.Regex
Expand Down Expand Up @@ -183,18 +185,18 @@ lazy val integers: Builtins = Map(
},
)

lazy val bytes: Builtins = Map(
builtin("effekt::show(Byte)") {
case As.Byte(n) :: Nil => Value.String(n.toString)
},
)

lazy val booleans: Builtins = Map(
builtin("effekt::not(Bool)") {
case As.Bool(x) :: Nil => Value.Bool(!x)
},
)

lazy val bytes: Builtins = Map(
builtin("effekt::show(Byte)") {
case As.Byte(b) :: Nil => Value.String(b.toHexString)
}
)

lazy val strings: Builtins = Map(
builtin("effekt::infixConcat(String, String)") {
case As.String(x) :: As.String(y) :: Nil => Value.String(x + y)
Expand Down Expand Up @@ -286,13 +288,13 @@ lazy val bytearrays: Builtins = Map(
case As.ByteArray(arr) :: As.Int(index) :: As.Byte(value) :: Nil => arr.update(index.toInt, value); Value.Unit()
},
builtin("bytearray::compare(ByteArray, ByteArray)") {
case As.ByteArray(arr1) :: As.ByteArray(arr2) :: Nil => Value.Int(java.util.Arrays.compare(arr1, arr2))
case As.ByteArray(arr1) :: As.ByteArray(arr2) :: Nil => Value.Int(java.util.Arrays.compare(arr1.map(_.toByte), arr2.map(_.toByte)))
},
builtin("bytearray::fromString(String)") {
case As.String(str) :: Nil => Value.ByteArray(str.getBytes("UTF-8"))
case As.String(str) :: Nil => Value.ByteArray(str.getBytes("UTF-8").map(UByte.unsafeFromByte))
},
builtin("bytearray::toString(ByteArray)") {
case As.ByteArray(arr) :: Nil => Value.String(new String(arr, "UTF-8"))
case As.ByteArray(arr) :: Nil => Value.String(new String(arr.map(_.toByte), "UTF-8"))
},
)

Expand Down Expand Up @@ -347,8 +349,8 @@ protected object As {
}
}
object Byte {
def unapply(v: Value): Option[scala.Byte] = v match {
case Value.Literal(value: scala.Byte) => Some(value)
def unapply(v: Value): Option[UByte] = v match {
case Value.Literal(value: Byte) => Some(UByte.unsafeFromByte(value))
case _ => None
}
}
Expand All @@ -371,7 +373,7 @@ protected object As {
}
}
object ByteArray {
def unapply(v: Value): Option[scala.Array[Byte]] = v match {
def unapply(v: Value): Option[scala.Array[UByte]] = v match {
case Value.ByteArray(array) => Some(array)
case _ => None
}
Expand Down
5 changes: 3 additions & 2 deletions effekt/shared/src/main/scala/effekt/core/vm/VM.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package core
package vm

import effekt.source.FeatureFlag
import effekt.util.UByte

import scala.annotation.tailrec

Expand All @@ -23,13 +24,13 @@ enum Value {
// TODO this could also be Pointer(Array | Ref)
case Array(array: scala.Array[Value])
case Ref(ref: Reference)
case ByteArray(array: scala.Array[Byte])
case ByteArray(array: scala.Array[UByte])
case Data(data: ValueType.Data, tag: Id, fields: List[Value])
case Boxed(block: Computation)
}
object Value {
def Int(v: Long): Value = Value.Literal(v)
def Byte(v: Byte): Value = Value.Literal(v)
def Byte(v: UByte): Value = Value.Literal(v)
def Bool(b: Boolean): Value = Value.Literal(b)
def Unit(): Value = Value.Literal(())
def Double(d: scala.Double): Value = Value.Literal(d)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package chez
import effekt.context.Context
import effekt.core.*
import effekt.symbols.{ Module, Symbol, TermSymbol, Wildcard }
import effekt.util.UByte
import effekt.util.paths.*
import effekt.util.messages.ErrorReporter
import kiama.output.PrettyPrinterTypes.Document
Expand Down Expand Up @@ -218,6 +219,7 @@ trait Transformer {

case Literal(s: String, _) => escape(s)
case Literal(b: Boolean, _) => if (b) chez.RawValue("#t") else chez.RawValue("#f")
case Literal(b: Byte, _) => chez.RawValue(UByte.unsafeFromByte(b).toInt.toString)
case l: Literal => chez.RawValue(l.value.toString)
case ValueVar(id, _) => chez.Variable(nameRef(id))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import cps.*
import core.Declaration
import effekt.symbols.Symbol
import effekt.core
import effekt.util.UByte
import effekt.util.messages.ErrorReporter

object TransformerCPS {
Expand Down Expand Up @@ -172,6 +173,7 @@ object TransformerCPS {
effekt.generator.chez.TransformerMonadic.escape(v)
case Literal(b: Boolean) =>
if (b) chez.RawValue("#t") else chez.RawValue("#f")
case Literal(b: Byte) => chez.RawValue(UByte.unsafeFromByte(b).toInt.toString)
case Literal(value) => chez.RawValue(value.toString())
case PureApp(id, vargs) => chez.Call(toChez(id), vargs.map(toChez))
case Make(_, tag, vargs) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import effekt.cps.*
import effekt.core.{Declaration, DeclarationContext, Id}
import effekt.cps.Variables.{all, free}
import effekt.cps.substitutions.Substitution
import effekt.util.UByte

import scala.collection.mutable

Expand Down Expand Up @@ -197,6 +198,7 @@ object TransformerCps extends Transformer {
case Expr.ValueVar(id) => nameRef(id)
case Expr.Literal(()) => $effekt.field("unit")
case Expr.Literal(s: String) => JsString(escape(s))
case Expr.Literal(b: Byte) => js.RawExpr(UByte.unsafeFromByte(b).toHexString)
case literal: Expr.Literal => js.RawExpr(literal.value.toString)
case Expr.PureApp(id, vargs) => inlineExtern(id, vargs)
case Expr.Make(data, tag, vargs) => js.New(nameRef(tag), vargs map toJS)
Expand Down
14 changes: 9 additions & 5 deletions effekt/shared/src/main/scala/effekt/machine/Transformer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package effekt
package machine

import effekt.context.Context
import effekt.core.substitutions.{ Substitution, substitute }
import effekt.core.{ Block, DeclarationContext, Toplevel, Id, given }
import effekt.symbols.{ Symbol, TermSymbol }
import effekt.core.substitutions.{Substitution, substitute}
import effekt.core.{Block, DeclarationContext, Id, Toplevel, given}
import effekt.symbols.{Symbol, TermSymbol}
import effekt.symbols.builtins.TState
import effekt.util.messages.ErrorReporter
import effekt.util.Trampoline
import effekt.symbols.ErrorMessageInterpolator
import effekt.util.UByte

import scala.annotation.tailrec


Expand Down Expand Up @@ -460,10 +462,12 @@ object Transformer {
LiteralInt(unboxed, value, Coerce(variable, unboxed, k(variable)))
}

case core.Literal(value: Int, core.Type.TByte) =>
case core.Literal(value: Byte, core.Type.TByte) =>
shift { k =>
val unboxed = Variable(freshName("byte"), Type.Byte())
LiteralByte(unboxed, value, Coerce(variable, unboxed, k(variable)))
val b = UByte.unsafeFromByte(value)
// TODO: Literal bytes could now also be just byte-sized in Machine (= use 'UByte'; they are byte-sized in LLVM anyway).
LiteralByte(unboxed, b.toInt, Coerce(variable, unboxed, k(variable)))
Comment on lines +468 to +470
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would prefer toUnsignedInt here instead of the newtype wrapping, but ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The biggest issue I have with toUnsignedInt is that you need to remember to use that whenever you see a Byte, which felt like a footgun (that I stumbled into like three times when making this PR in the first place).
The alternative is weird since we can't match on UByte as a type, so it's all quite unsatisfactory :/

}

case core.Literal(v: Double, core.Type.TDouble) =>
Expand Down
3 changes: 3 additions & 0 deletions effekt/shared/src/main/scala/effekt/source/Tree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package source

import effekt.context.Context
import effekt.symbols.Symbol
import effekt.util.UByte
import kiama.util.{Source, StringSource}

import scala.annotation.tailrec

/**
Expand Down Expand Up @@ -569,6 +571,7 @@ export Term.*
// Smart Constructors for literals
// -------------------------------
def UnitLit(span: Span): Literal = Literal((), symbols.builtins.TUnit, span)
def ByteLit(value: UByte, span: Span) : Literal = Literal(value, symbols.builtins.TByte, span)
def IntLit(value: Long, span: Span): Literal = Literal(value, symbols.builtins.TInt, span)
def BooleanLit(value: Boolean, span: Span): Literal = Literal(value, symbols.builtins.TBoolean, span)
def DoubleLit(value: Double, span: Span): Literal = Literal(value, symbols.builtins.TDouble, span)
Expand Down
Loading
Loading