Skip to content

Commit

Permalink
Implement imaginary literals (#194)
Browse files Browse the repository at this point in the history
OQ3 has no imaginary type, only complex type (I don't know of any general
purpose language that differs in this respect). But it is convenient to
implement and use imaginary literals in order to handle expressions like
`10 im`, `10im`, `12.3 im`, `12.3im`, etc.

We already have implemented all the machinery for timing literals. Lexing and
maniuplating imaginary literal is structurally identical, so we can use the
machinery for timing literals at (almost) no extra cost in coding or complexity.
A disadvantage is that imaginary and timing literals don't belong in the same
category. IDK, we could rename everything to `timing_or_imaginary.

An imaginary float literal can be stored and manipulated in exactly the same
way, with the same semantics as a real float literal. The exception is the
last step of specifying the type. For this reason we break with the precedent
of all other variants of enums in asg.rs by reusing a the struct field for
real literals. That is, we have:

```
Float(FloatLiteral),
ImaginaryFloat(FloatLiteral),
```
The type (float or complex float) is stored at a higher level as it is for
all expressions.
  • Loading branch information
jlapeyre committed Mar 26, 2024
1 parent 5ccf7af commit a421dff
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 9 deletions.
13 changes: 10 additions & 3 deletions crates/oq3_lexer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ impl Cursor<'_> {
// If this a timing (or duration) literal, we will parse the
// time unit as another token. So we don't eat the suffix if it
// is a time unit.
if !self.has_timing_suffix() {
if !self.has_timing_or_imaginary_suffix() {
self.eat_literal_suffix();
}
TokenKind::Literal {
Expand Down Expand Up @@ -760,12 +760,19 @@ impl Cursor<'_> {
self.eat_identifier();
}

fn has_timing_suffix(&mut self) -> bool {
fn has_timing_or_imaginary_suffix(&mut self) -> bool {
if self.first() == 's' {
return true;
} else {
// TODO: greek mu is encoded in more than one way. We only get one here.
for (f, s) in [('d', 't'), ('n', 's'), ('u', 's'), ('m', 's'), ('µ', 's')] {
for (f, s) in [
('d', 't'),
('n', 's'),
('u', 's'),
('m', 's'),
('µ', 's'),
('i', 'm'),
] {
if self.first() == f && self.second() == s {
return true;
}
Expand Down
8 changes: 6 additions & 2 deletions crates/oq3_parser/src/grammar/expressions/atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub(crate) const LITERAL_FIRST: TokenSet = TokenSet::new(&[
T![false],
]);

// Also for imaginary literals
const TIMING_LITERAL_FIRST: TokenSet = TokenSet::new(&[INT_NUMBER, FLOAT_NUMBER]);

pub(crate) fn literal(p: &mut Parser<'_>) -> Option<CompletedMarker> {
Expand All @@ -28,15 +29,18 @@ pub(crate) fn literal(p: &mut Parser<'_>) -> Option<CompletedMarker> {
}
let m = p.start();
// If the first token after the current one is an identifer, then the only
// syntactically correct construct is a timing literal. Note that `s` is a
// syntactically correct construct is a timing or imaginary literal. Note that `s` is a
// valid suffix for both a timing literal and a variable identifier, we
// can't make `s` a keyword token. This is because, in the parser, I don't
// know how to make a string either an identifer or "keyword"
// depending on context. So we parse an identifier and validate in validate.rs.
if matches!(p.nth(1), IDENT) {
if !p.at_ts(TIMING_LITERAL_FIRST) {
p.error("Timing literal must begin with an integer or float literal");
p.error("Timing and imaginary literals must begin with an integer or float literal");
}
// We don't have access to the text of the identifier here, so we can't distinguish
// timing literals from imaginary literals. We tag everything TIMING_LITERAL
// Later in semantic analysis we separate imaginary literals from timing literals.
let m2 = p.start(); // TIMING_LITERAL
p.bump_any(); // The numeric literal
m.complete(p, LITERAL);
Expand Down
21 changes: 21 additions & 0 deletions crates/oq3_semantics/src/asg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,8 @@ pub enum Literal {
Bool(BoolLiteral),
Int(IntLiteral),
Float(FloatLiteral),
ImaginaryInt(IntLiteral),
ImaginaryFloat(FloatLiteral),
BitString(BitStringLiteral),
TimingIntLiteral(TimingIntLiteral),
TimingFloatLiteral(TimingFloatLiteral),
Expand Down Expand Up @@ -951,6 +953,14 @@ impl IntLiteral {
pub fn to_texpr(self) -> TExpr {
TExpr::new(self.to_expr(), Type::Int(Some(128), IsConst::True))
}

pub fn to_imaginary_expr(self) -> Expr {
Expr::Literal(Literal::ImaginaryInt(self))
}

pub fn to_imaginary_texpr(self) -> TExpr {
TExpr::new(self.to_imaginary_expr(), Type::Int(Some(64), IsConst::True))
}
}

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -1076,6 +1086,17 @@ impl FloatLiteral {
pub fn to_texpr(self) -> TExpr {
TExpr::new(self.to_expr(), Type::Float(Some(64), IsConst::True))
}

pub fn to_imaginary_expr(self) -> Expr {
Expr::Literal(Literal::ImaginaryFloat(self))
}

pub fn to_imaginary_texpr(self) -> TExpr {
TExpr::new(
self.to_imaginary_expr(),
Type::Complex(Some(64), IsConst::True),
)
}
}

#[derive(Clone, Debug, PartialEq)]
Expand Down
26 changes: 24 additions & 2 deletions crates/oq3_semantics/src/syntax_to_semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@ fn from_expr(expr_maybe: Option<synast::Expr>, context: &mut Context) -> Option<
let expr = expr_maybe?;
match expr {
// FIXME: Ugh. could clean up logic here
// It is convenient to rewrite literals wrapped in unary minus as literals
// with negative values.
synast::Expr::PrefixExpr(prefix_expr) => {
match prefix_expr.op_kind() {
Some(synast::UnaryOp::Neg) => {
Expand Down Expand Up @@ -612,13 +614,34 @@ fn from_expr(expr_maybe: Option<synast::Expr>, context: &mut Context) -> Option<

synast::Expr::Literal(ref literal) => from_literal(literal),

// We also handle imaginary literals here along with timing literals.
// This makes no sense on the level of semantics. But at all eariler points,
// from lexing to here, it is trivial to treat the imaginary suffix `im` as
// if it were a timing suffix. We could try to fix this, but it would add
// code paths and complexity from lexing on up.
synast::Expr::TimingLiteral(ref timing_literal) => {
let time_unit = match timing_literal.time_unit().unwrap() {
let ast_time_unit = timing_literal.time_unit().unwrap();
if matches!(ast_time_unit, synast::TimeUnit::Imaginary) {
return match timing_literal.literal().unwrap().kind() {
synast::LiteralKind::IntNumber(int_num) => {
let num = int_num.value_u128().unwrap();
Some(asg::IntLiteral::new(num, true).to_imaginary_texpr())
}
synast::LiteralKind::FloatNumber(float_num) => {
let num = float_num.value().unwrap();
Some(asg::FloatLiteral::new(num).to_imaginary_texpr())
}
_ => panic!("You have found a bug in oq3_syntax or oq3_parser"),
};
}
let time_unit = match ast_time_unit {
synast::TimeUnit::Second => asg::TimeUnit::Second,
synast::TimeUnit::MilliSecond => asg::TimeUnit::MilliSecond,
synast::TimeUnit::MicroSecond => asg::TimeUnit::MicroSecond,
synast::TimeUnit::NanoSecond => asg::TimeUnit::NanoSecond,
synast::TimeUnit::Cycle => asg::TimeUnit::Cycle,
// Imaginary was handled above.
synast::TimeUnit::Imaginary => unreachable!(),
};
match timing_literal.literal().unwrap().kind() {
synast::LiteralKind::IntNumber(int_num) => {
Expand All @@ -629,7 +652,6 @@ fn from_expr(expr_maybe: Option<synast::Expr>, context: &mut Context) -> Option<
let num = float_num.value().unwrap();
Some(asg::TimingFloatLiteral::new(num, true, time_unit).to_texpr())
}

_ => panic!("You have found a bug in oq3_syntax or oq3_parser"),
}
}
Expand Down
51 changes: 51 additions & 0 deletions crates/oq3_semantics/tests/from_string_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,3 +791,54 @@ yy = xx;
SemanticErrorKind::IncompatibleTypesError
));
}

#[test]
fn test_from_string_imaginary_int_literal_1() {
let code = r##"
10 im;
"##;
let (program, errors, _symbol_table) = parse_string(code);
assert_eq!(errors.len(), 0);
let stmt = &program[0];
match stmt {
asg::Stmt::ExprStmt(texpr) => match texpr.expression() {
asg::Expr::Literal(asg::Literal::ImaginaryInt(_)) => (),
_ => unreachable!(),
},
_ => unreachable!(),
};
}

#[test]
fn test_from_string_imaginary_int_literal_2() {
let code = r##"
10im;
"##;
let (program, errors, _symbol_table) = parse_string(code);
assert_eq!(errors.len(), 0);
let stmt = &program[0];
match stmt {
asg::Stmt::ExprStmt(texpr) => match texpr.expression() {
asg::Expr::Literal(asg::Literal::ImaginaryInt(_)) => (),
_ => unreachable!(),
},
_ => unreachable!(),
};
}

#[test]
fn test_from_string_imaginary_float_literal_1() {
let code = r##"
12.3 im;
"##;
let (program, errors, _symbol_table) = parse_string(code);
assert_eq!(errors.len(), 0);
let stmt = &program[0];
match stmt {
asg::Stmt::ExprStmt(texpr) => match texpr.expression() {
asg::Expr::Literal(asg::Literal::ImaginaryFloat(_)) => (),
_ => unreachable!(),
},
_ => unreachable!(),
};
}
2 changes: 2 additions & 0 deletions crates/oq3_syntax/src/ast/expr_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ pub enum TimeUnit {
MicroSecond,
Second,
Cycle,
Imaginary,
}

impl ast::TimingLiteral {
Expand All @@ -390,6 +391,7 @@ impl ast::TimingLiteral {
"us" | "µs" => Some(TimeUnit::MicroSecond),
"ns" => Some(TimeUnit::NanoSecond),
"dt" => Some(TimeUnit::Cycle),
"im" => Some(TimeUnit::Imaginary),
_ => None,
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/oq3_syntax/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ fn validate_literal(literal: ast::Literal, acc: &mut Vec<SyntaxError>) {
fn validate_timing_literal(timing_literal: ast::TimingLiteral, errors: &mut Vec<SyntaxError>) {
if !matches!(
timing_literal.identifier().unwrap().text().as_str(),
"s" | "ms" | "us" | "µs" | "ns" | "dt"
"s" | "ms" | "us" | "µs" | "ns" | "dt" | "im"
) {
errors.push(SyntaxError::new(
"Time unit must be one of 's', 'ms', 'us', 'µs', 'ns', or 'dt'",
"Expected 'im', or one of the time units 's', 'ms', 'us', 'µs', 'ns', or 'dt'",
timing_literal.syntax().text_range(),
));
}
Expand Down

0 comments on commit a421dff

Please sign in to comment.