started ir
This commit is contained in:
parent
47c6ccfa6b
commit
6175f976a3
|
@ -1,3 +1,2 @@
|
||||||
Point :: struct {x: integer};
|
a : i32 = 10;
|
||||||
a : integer : 10;
|
b := 20 + a;
|
||||||
p :: Point{x: a};
|
|
||||||
|
|
17
src/ir.rs
Normal file
17
src/ir.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use crate::{semantic::Type, BinaryOp, Immediate, UnaryOp};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum IR {
|
||||||
|
BinOp { op: BinaryOp, type_: Type },
|
||||||
|
UnOp { op: UnaryOp, type_: Type },
|
||||||
|
Imm(Immediate),
|
||||||
|
NewLocal { uid: usize, type_: Type },
|
||||||
|
AssignLocal { uid: usize },
|
||||||
|
AccessLocal { uid: usize },
|
||||||
|
NewGlobal { uid: usize, type_: Type },
|
||||||
|
AssignGlobal { uid: usize },
|
||||||
|
AccessGlobal { uid: usize },
|
||||||
|
StartFunc { uid: usize },
|
||||||
|
NewParam { uid: usize, type_: Type },
|
||||||
|
EndFunc,
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod err;
|
mod err;
|
||||||
mod frontend;
|
mod frontend;
|
||||||
|
mod ir;
|
||||||
mod lookahead;
|
mod lookahead;
|
||||||
mod parse;
|
mod parse;
|
||||||
mod semantic;
|
mod semantic;
|
||||||
|
|
|
@ -1,5 +1,80 @@
|
||||||
mod primitives;
|
mod primitives;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
|
use crate::err::*;
|
||||||
pub use primitives::*;
|
pub use primitives::*;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Symbol {
|
||||||
|
Var(String, Type, bool),
|
||||||
|
Type(String, Type),
|
||||||
|
BlockStart,
|
||||||
|
FuncStart,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SymbolTable {
|
||||||
|
syms: Vec<Symbol>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolTable {
|
||||||
|
fn define_var(&mut self, name: String, type_: Type, mutable: bool) {
|
||||||
|
self.syms.push(Symbol::Var(name, type_, mutable));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn define_type(&mut self, name: String, type_: Type) {
|
||||||
|
self.syms.push(Symbol::Type(name, type_));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_func(&mut self) {
|
||||||
|
self.syms.push(Symbol::FuncStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_func(&mut self) {
|
||||||
|
while !self.syms.is_empty() {
|
||||||
|
if let Some(Symbol::FuncStart) = self.syms.pop() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable!("Tried to exit global scope in symbol table")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_block(&mut self) {
|
||||||
|
self.syms.push(Symbol::BlockStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_block(&mut self) {
|
||||||
|
while !self.syms.is_empty() {
|
||||||
|
if let Some(Symbol::BlockStart) = self.syms.pop() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable!("Tried to exit global scope in symbol table")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_var(&self, name: &str) -> Result<(Type, bool)> {
|
||||||
|
for s in self.syms.iter().rev() {
|
||||||
|
if let Symbol::Var(name2, type_, mutable) = s {
|
||||||
|
if name == name2 {
|
||||||
|
return Ok((type_.clone(), *mutable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error().reason(format!("Identifier {name} is not defined"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_type(&self, name: &str) -> Result<Type> {
|
||||||
|
for s in self.syms.iter().rev() {
|
||||||
|
if let Symbol::Type(name2, t) = s {
|
||||||
|
if name == name2 {
|
||||||
|
return Ok(t.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(p) = Primitive::from_string(name) {
|
||||||
|
return Ok(Type::Prim(p));
|
||||||
|
}
|
||||||
|
error().reason(format!("Type {name} is not defined"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
BinaryOp, Expression, ExpressionKind, Immediate, Parameter, Statement,
|
semantic::SymbolTable, BinaryOp, Expression, ExpressionKind, Immediate, Parameter, Statement,
|
||||||
StatementKind, UnaryOp,
|
StatementKind, UnaryOp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,14 +45,6 @@ impl PartialEq for Type {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Type {
|
impl Type {
|
||||||
pub fn from_string(value: &str) -> Self {
|
|
||||||
if let Some(p) = Primitive::from_string(value) {
|
|
||||||
Type::Prim(p)
|
|
||||||
} else {
|
|
||||||
Type::Ambiguous
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn binary_op(lhs: &Type, op: BinaryOp, rhs: &Type) -> Result<Type> {
|
pub fn binary_op(lhs: &Type, op: BinaryOp, rhs: &Type) -> Result<Type> {
|
||||||
use Type as t;
|
use Type as t;
|
||||||
let e = error().reason(format!(
|
let e = error().reason(format!(
|
||||||
|
@ -62,7 +54,7 @@ impl Type {
|
||||||
(t::Prim(a), t::Prim(b)) => {
|
(t::Prim(a), t::Prim(b)) => {
|
||||||
let p = Primitive::binary_op(*a, op, *b)?;
|
let p = Primitive::binary_op(*a, op, *b)?;
|
||||||
Ok(t::Prim(p))
|
Ok(t::Prim(p))
|
||||||
},
|
}
|
||||||
_ => e,
|
_ => e,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,73 +79,17 @@ impl Type {
|
||||||
(Ambiguous, t) => Some(t.clone()),
|
(Ambiguous, t) => Some(t.clone()),
|
||||||
(Prim(p1), Prim(p2)) => {
|
(Prim(p1), Prim(p2)) => {
|
||||||
let (p1, p2) = Primitive::coerce_ambiguous(*p1, *p2);
|
let (p1, p2) = Primitive::coerce_ambiguous(*p1, *p2);
|
||||||
if p1 != p2 { None } else { Some(Type::Prim(p1)) }
|
if p1 != p2 {
|
||||||
},
|
None
|
||||||
|
} else {
|
||||||
|
Some(Type::Prim(p1))
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum Symbol {
|
|
||||||
Var(String, Type, bool),
|
|
||||||
Type(String, Type),
|
|
||||||
BlockStart,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct SymbolTable {
|
|
||||||
syms: Vec<Symbol>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SymbolTable {
|
|
||||||
fn define_var(&mut self, name: String, type_: Type, mutable: bool) {
|
|
||||||
self.syms.push(Symbol::Var(name, type_, mutable));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn define_type(&mut self, name: String, type_: Type) {
|
|
||||||
self.syms.push(Symbol::Type(name, type_));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_block(&mut self) {
|
|
||||||
self.syms.push(Symbol::BlockStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_block(&mut self) {
|
|
||||||
while !self.syms.is_empty() {
|
|
||||||
if let Some(Symbol::BlockStart) = self.syms.pop() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unreachable!("Tried to exit global scope in symbol table")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_var(&self, name: &str) -> Result<(Type, bool)> {
|
|
||||||
for s in self.syms.iter().rev() {
|
|
||||||
if let Symbol::Var(name2, type_, mutable) = s {
|
|
||||||
if name == name2 {
|
|
||||||
return Ok((type_.clone(), *mutable));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
error().reason(format!("Identifier {name} is not defined"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_type(&self, name: &str) -> Result<Type> {
|
|
||||||
for s in self.syms.iter().rev() {
|
|
||||||
if let Symbol::Type(name2, t) = s {
|
|
||||||
if name == name2 {
|
|
||||||
return Ok(t.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(p) = Primitive::from_string(name) {
|
|
||||||
return Ok(Type::Prim(p));
|
|
||||||
}
|
|
||||||
error().reason(format!("Type {name} is not defined"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn typecheck(program: Vec<Statement>) -> Vec<Statement> {
|
pub fn typecheck(program: Vec<Statement>) -> Vec<Statement> {
|
||||||
use StatementKind as s;
|
use StatementKind as s;
|
||||||
let mut table = SymbolTable { syms: vec![] };
|
let mut table = SymbolTable { syms: vec![] };
|
||||||
|
@ -171,10 +107,7 @@ pub fn typecheck(program: Vec<Statement>) -> Vec<Statement> {
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
fn statement(
|
fn statement(mut stmt: Box<Statement>, table: &mut SymbolTable) -> Result<Box<Statement>> {
|
||||||
mut stmt: Box<Statement>,
|
|
||||||
table: &mut SymbolTable,
|
|
||||||
) -> Result<Box<Statement>> {
|
|
||||||
use Primitive as p;
|
use Primitive as p;
|
||||||
use StatementKind as s;
|
use StatementKind as s;
|
||||||
match stmt.kind {
|
match stmt.kind {
|
||||||
|
@ -190,11 +123,10 @@ fn statement(
|
||||||
None => Type::Ambiguous,
|
None => Type::Ambiguous,
|
||||||
};
|
};
|
||||||
let value = expression(value.into(), table)?;
|
let value = expression(value.into(), table)?;
|
||||||
let type_actual =
|
let type_actual = Type::coerce(&type_expect, &value.type_).reason(format!(
|
||||||
Type::coerce(&type_expect, &value.type_).reason(format!(
|
"Expected type '{:?}', found type '{:?}'",
|
||||||
"Expected type '{:?}', found type '{:?}'",
|
type_expect, value.type_
|
||||||
type_expect, value.type_
|
))?;
|
||||||
))?;
|
|
||||||
// Check that structs are const
|
// Check that structs are const
|
||||||
if let Type::Struct(_) = type_actual {
|
if let Type::Struct(_) = type_actual {
|
||||||
if mutable {
|
if mutable {
|
||||||
|
@ -222,7 +154,7 @@ fn statement(
|
||||||
value: *value,
|
value: *value,
|
||||||
mutable,
|
mutable,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
s::Assignment { name, value } => {
|
s::Assignment { name, value } => {
|
||||||
let (type_, mutable) = table.get_var(&name).span(&stmt.span)?;
|
let (type_, mutable) = table.get_var(&name).span(&stmt.span)?;
|
||||||
// Check that it is mutable
|
// Check that it is mutable
|
||||||
|
@ -240,7 +172,7 @@ fn statement(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
stmt.kind = s::Assignment { name, value };
|
stmt.kind = s::Assignment { name, value };
|
||||||
},
|
}
|
||||||
s::If {
|
s::If {
|
||||||
predicate,
|
predicate,
|
||||||
block,
|
block,
|
||||||
|
@ -271,7 +203,7 @@ fn statement(
|
||||||
else_,
|
else_,
|
||||||
};
|
};
|
||||||
table.end_block();
|
table.end_block();
|
||||||
},
|
}
|
||||||
s::While { predicate, block } => {
|
s::While { predicate, block } => {
|
||||||
table.start_block();
|
table.start_block();
|
||||||
let predicate = *expression(predicate.into(), table)?;
|
let predicate = *expression(predicate.into(), table)?;
|
||||||
|
@ -292,10 +224,10 @@ fn statement(
|
||||||
block: new_block,
|
block: new_block,
|
||||||
};
|
};
|
||||||
table.end_block();
|
table.end_block();
|
||||||
},
|
}
|
||||||
s::Print(e) => {
|
s::Print(e) => {
|
||||||
stmt.kind = s::Print(*expression(e.into(), table)?);
|
stmt.kind = s::Print(*expression(e.into(), table)?);
|
||||||
},
|
}
|
||||||
s::Expression(mut e) => {
|
s::Expression(mut e) => {
|
||||||
use ExpressionKind as e;
|
use ExpressionKind as e;
|
||||||
let is_func = if let e::Function { params, .. } = &mut e.kind {
|
let is_func = if let e::Function { params, .. } = &mut e.kind {
|
||||||
|
@ -313,7 +245,7 @@ fn statement(
|
||||||
if is_func {
|
if is_func {
|
||||||
table.end_block();
|
table.end_block();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
s::Block(block) => {
|
s::Block(block) => {
|
||||||
table.start_block();
|
table.start_block();
|
||||||
let mut new_block = vec![];
|
let mut new_block = vec![];
|
||||||
|
@ -322,16 +254,13 @@ fn statement(
|
||||||
}
|
}
|
||||||
stmt.kind = s::Block(new_block);
|
stmt.kind = s::Block(new_block);
|
||||||
table.end_block();
|
table.end_block();
|
||||||
},
|
}
|
||||||
s::Error(e) => return Err(e),
|
s::Error(e) => return Err(e),
|
||||||
}
|
}
|
||||||
Ok(stmt)
|
Ok(stmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expression(
|
fn expression(mut expr: Box<Expression>, table: &SymbolTable) -> Result<Box<Expression>> {
|
||||||
mut expr: Box<Expression>,
|
|
||||||
table: &SymbolTable,
|
|
||||||
) -> Result<Box<Expression>> {
|
|
||||||
use ExpressionKind as e;
|
use ExpressionKind as e;
|
||||||
use Immediate as i;
|
use Immediate as i;
|
||||||
use Primitive as p;
|
use Primitive as p;
|
||||||
|
@ -349,19 +278,19 @@ fn expression(
|
||||||
let type_ = Type::binary_op(&left.type_, op, &right.type_)?;
|
let type_ = Type::binary_op(&left.type_, op, &right.type_)?;
|
||||||
expr.kind = e::Binary { left, right, op };
|
expr.kind = e::Binary { left, right, op };
|
||||||
type_
|
type_
|
||||||
},
|
}
|
||||||
e::Unary { op, child } => {
|
e::Unary { op, child } => {
|
||||||
let child = expression(child, table)?;
|
let child = expression(child, table)?;
|
||||||
let type_ = Type::unary_op(op, &child.type_)?;
|
let type_ = Type::unary_op(op, &child.type_)?;
|
||||||
expr.kind = e::Unary { child, op };
|
expr.kind = e::Unary { child, op };
|
||||||
type_
|
type_
|
||||||
},
|
}
|
||||||
e::Parenthesis(inner) => {
|
e::Parenthesis(inner) => {
|
||||||
let inner = expression(inner, table)?;
|
let inner = expression(inner, table)?;
|
||||||
let type_ = inner.type_.clone();
|
let type_ = inner.type_.clone();
|
||||||
expr.kind = e::Parenthesis(inner);
|
expr.kind = e::Parenthesis(inner);
|
||||||
type_
|
type_
|
||||||
},
|
}
|
||||||
e::Function {
|
e::Function {
|
||||||
mut params,
|
mut params,
|
||||||
returns_str,
|
returns_str,
|
||||||
|
@ -385,15 +314,57 @@ fn expression(
|
||||||
params: params.into_iter().map(|p| p.type_actual).collect(),
|
params: params.into_iter().map(|p| p.type_actual).collect(),
|
||||||
returns: returns_actual.into(),
|
returns: returns_actual.into(),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
e::Call { callee, mut args } => {
|
||||||
|
let callee = expression(callee, table)?;
|
||||||
|
// Check that this is actually a function
|
||||||
|
let Type::Function {
|
||||||
|
ref params,
|
||||||
|
ref returns,
|
||||||
|
} = callee.type_
|
||||||
|
else {
|
||||||
|
return error()
|
||||||
|
.reason(format!("Cannot call type {:?}", callee.type_))
|
||||||
|
.span(&callee.span);
|
||||||
|
};
|
||||||
|
// Check for correct number of args
|
||||||
|
if params.len() != args.len() {
|
||||||
|
return error()
|
||||||
|
.reason(format!(
|
||||||
|
"Wrong number of arguments, function expects {}, found {}",
|
||||||
|
params.len(),
|
||||||
|
args.len()
|
||||||
|
))
|
||||||
|
.span(&callee.span);
|
||||||
|
}
|
||||||
|
// Check for correct arg types
|
||||||
|
for (expect, actual) in params.iter().zip(args.iter_mut()) {
|
||||||
|
*actual = *expression(actual.clone().into(), table)?;
|
||||||
|
let coerced_type = Type::coerce(expect, &actual.type_);
|
||||||
|
println!("{:?}, {:?}, {coerced_type:?}", expect, actual.type_);
|
||||||
|
if let Some(t) = coerced_type {
|
||||||
|
actual.type_ = t;
|
||||||
|
} else {
|
||||||
|
return error()
|
||||||
|
.reason(format!(
|
||||||
|
"Expected type {expect:?}, found {:?}",
|
||||||
|
actual.type_
|
||||||
|
))
|
||||||
|
.span(&actual.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let returns = *returns.clone();
|
||||||
|
expr.kind = e::Call { callee, args };
|
||||||
|
returns
|
||||||
|
}
|
||||||
e::Struct(mut params) => {
|
e::Struct(mut params) => {
|
||||||
for p in &mut params {
|
for p in &mut params {
|
||||||
p.type_actual = table.get_type(&p.type_str).span(&expr.span)?;
|
p.type_actual = table.get_type(&p.type_str).span(&expr.span)?;
|
||||||
}
|
}
|
||||||
expr.kind = e::Struct(params.clone());
|
expr.kind = e::Struct(params.clone());
|
||||||
Type::Struct(params)
|
Type::Struct(params)
|
||||||
},
|
}
|
||||||
e::StructLiteral { name, mut args } => {
|
e::StructLiteral { name, args } => {
|
||||||
let type_ = table.get_type(&name).span(&expr.span)?;
|
let type_ = table.get_type(&name).span(&expr.span)?;
|
||||||
let Type::Struct(params) = type_ else {
|
let Type::Struct(params) = type_ else {
|
||||||
return error().reason(format!(
|
return error().reason(format!(
|
||||||
|
@ -403,7 +374,7 @@ fn expression(
|
||||||
};
|
};
|
||||||
if args.len() != params.len() {
|
if args.len() != params.len() {
|
||||||
return error().reason(format!(
|
return error().reason(format!(
|
||||||
"Incorrect number of parameters for struct '{}', expected {} and \
|
"Incorrect number of parameters for struct '{}'; expected {}, \
|
||||||
found {}",
|
found {}",
|
||||||
name,
|
name,
|
||||||
params.len(),
|
params.len(),
|
||||||
|
@ -430,9 +401,12 @@ fn expression(
|
||||||
.span(&argexpr.span);
|
.span(&argexpr.span);
|
||||||
}
|
}
|
||||||
let argspan = argexpr.span;
|
let argspan = argexpr.span;
|
||||||
let arg = *expression(argexpr.clone().into(), table)
|
let mut arg = *expression(argexpr.clone().into(), table)
|
||||||
.trace_span(expr.span, "while parsing struct literal")?;
|
.trace_span(expr.span, "while parsing struct literal")?;
|
||||||
if &arg.type_ != ptype {
|
let coerced_type = Type::coerce(ptype, &arg.type_);
|
||||||
|
if let Some(t) = coerced_type {
|
||||||
|
arg.type_ = t;
|
||||||
|
} else {
|
||||||
return error()
|
return error()
|
||||||
.reason(format!(
|
.reason(format!(
|
||||||
"In struct literal, expected type '{ptype:?}', found '{:?}",
|
"In struct literal, expected type '{ptype:?}', found '{:?}",
|
||||||
|
@ -447,43 +421,7 @@ fn expression(
|
||||||
args: new_args,
|
args: new_args,
|
||||||
};
|
};
|
||||||
Type::Struct(params)
|
Type::Struct(params)
|
||||||
},
|
}
|
||||||
e::Call { callee, args } => {
|
|
||||||
let callee = expression(callee, table)?;
|
|
||||||
// Check that this is actually a function
|
|
||||||
let Type::Function {
|
|
||||||
ref params,
|
|
||||||
ref returns,
|
|
||||||
} = callee.type_
|
|
||||||
else {
|
|
||||||
return error()
|
|
||||||
.reason(format!("Cannot call type {:?}", callee.type_))
|
|
||||||
.span(&callee.span);
|
|
||||||
};
|
|
||||||
// Check for correct number of args
|
|
||||||
if params.len() != args.len() {
|
|
||||||
return error()
|
|
||||||
.reason(format!(
|
|
||||||
"Wrong number of arguments, function expects {}",
|
|
||||||
params.len()
|
|
||||||
))
|
|
||||||
.span(&callee.span);
|
|
||||||
}
|
|
||||||
// Check for correct arg types
|
|
||||||
for (expect, actual) in params.iter().zip(args.iter()) {
|
|
||||||
if *expect != actual.type_ {
|
|
||||||
return error()
|
|
||||||
.reason(format!(
|
|
||||||
"Expected type {expect:?}, found {:?}",
|
|
||||||
actual.type_
|
|
||||||
))
|
|
||||||
.span(&actual.span);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let returns = *returns.clone();
|
|
||||||
expr.kind = e::Call { callee, args };
|
|
||||||
returns
|
|
||||||
},
|
|
||||||
e::Field { namespace, field } => {
|
e::Field { namespace, field } => {
|
||||||
let namespace = expression(namespace, table)?;
|
let namespace = expression(namespace, table)?;
|
||||||
// Check that namespace is struct
|
// Check that namespace is struct
|
||||||
|
@ -515,7 +453,7 @@ fn expression(
|
||||||
.span(&field.span)?;
|
.span(&field.span)?;
|
||||||
expr.kind = e::Field { namespace, field };
|
expr.kind = e::Field { namespace, field };
|
||||||
type_
|
type_
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
expr.type_ = type_;
|
expr.type_ = type_;
|
||||||
Ok(expr)
|
Ok(expr)
|
||||||
|
|
Loading…
Reference in a new issue