Compare commits

...

2 Commits

Author SHA1 Message Date
5f316cf17d add new features to readme 2022-02-26 12:14:31 +01:00
e608d3bb4b implement const 2022-02-26 11:46:38 +01:00
8 changed files with 274 additions and 91 deletions

View File

@@ -56,6 +56,14 @@ Then run it on [MicroW8](https://exoticorn.github.io/microw8/v0.1pre2)
*/ */
``` ```
### Include
Other sourcefiles can be included with the `include` top level statement:
```
include "platform_imports.cwa"
```
### Types ### Types
There are four types in WebAssembly and therefore CurlyWas: There are four types in WebAssembly and therefore CurlyWas:
@@ -138,6 +146,18 @@ can use it. However, exporting global variable is not yet supported in CurlyWas.
The type is optional, if missing it is inferred from the init value. The type is optional, if missing it is inferred from the init value.
### Constants
Constants can be declared in the global scope:
```
const name[: type] = value;
```
`value` has to be an expression evaluating to a constant value. It may reference other constants.
The type is optional, but if given has to match the type of `value`.
### Functions ### Functions
Functions look like this: Functions look like this:
@@ -296,15 +316,65 @@ non-zero integer.
#### Memory load/store #### Memory load/store
To read from memory you specify a memory location as `base?offset` or `base!offset`. `?` reads a byte and `!` reads a 32bit word. To read from memory you specify a memory location as `base?offset`, `base!offset` or `base$offset`. `?` reads a byte, `!` reads a 32bit word
and `$` reads a 32bit float.
`base` can be any expression that evaluates to an `i32` while `offset` has to be a constant `i32` value. The effective memory address is the sum of both. `base` can be any expression that evaluates to an `i32` while `offset` has to be a constant `i32` value. The effective memory address is the sum of both.
Writing to memory looks just like an assignment to a memory location: `base?offset = expressoin` and `base!offset = expression`. Writing to memory looks just like an assignment to a memory location: `base?offset = expression`, `base!offset = expression` and `base$offset = expression`.
When reading/writing 32bit words you need to make sure the address is 4-byte aligned. When reading/writing 32bit words you need to make sure the address is 4-byte aligned.
These compile to `i32.load8_u`, `i32.load`, `i32.store8` and `i32.store`. Other WASM load/store instructions will be implemented as intrinsics, but aren't yet. These compile to `i32.load8_u`, `i32.load`, `f32.load`, `i32.store8`, `i32.store` and `f32.store`.
In addition, all wasm memory instructions are available as intrinsics:
```
<load-ins>(<base-address>[, <offset>, [<align>]])
offset defaults to 0, align to the natural alignment: 0 for 8bit loads, 1 for 16bit, 2 for 32 bit and 3 for 64bit.
```
with `<load-ins>` being one of `i32.load`, `i32.load8_u`, `i32.load8_s`, `i32.load16_u`, `i32.load16_s`,
`i64.load`, `i64.load8_u`, `i64.load8_s`, `i64.load16_u`, `i64.load16_s`, `i32.load32_u`, `i32.load32_s`,
`f32.load` and `f64.load`.
```
<store-ins>(<value>, <base-address>[, <offset>, [<align>]])
offset and align defaults are the same as the load intrinsics.
```
with `<store-ins>` being one of `i32.store`, `i32.store8`, `i32.store16`, `i64.store`, `i64.store8`,
`i64.store16`, `i64.store32`, `f32.store` and `f64.store`.
#### Data
Data sections are written in `data` blocks:
```
data <address> {
...
}
```
The content of such a block is loaded at the given address at module start.
Inside the data block you can include 8, 16, 32, 64, f32 or f64 values:
```
i8(1, 255) i16(655350) i32(0x12345678) i64(0x1234567890abcdefi64) f32(1.0, 3.141) f64(0.5f64)
```
Strings:
```
"First line" i8(13, 10) "Second line"
```
And binary files:
```
file("font.bin")
```
#### Advanced sequencing #### Advanced sequencing

View File

@@ -1,29 +1,8 @@
import "env.memory" memory(4); include "uw8.cwa"
import "env.pow" fn pow(f32, f32) -> f32; const SWEETY = PALETTE + 192 * 4;
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
import "env.atan2" fn atan2(f32, f32) -> f32;
import "env.tan" fn tan(f32) -> f32;
import "env.atan" fn atan(f32) -> f32;
import "env.rectangle" fn rect(f32, f32, f32, f32, i32);
//export fn tic(time: i32) { export fn upd() {
// let i: i32;
// loop pixels {
// let lazy x = (i % 320) as f32 - 160.5;
// let lazy y = (i / 320 - 120) as f32;
//
// let lazy dist = 4000 as f32 / sqrt(x*x + y*y + 10 as f32);
// let lazy angle = atan2(x, y) * (64.0 / 3.141);
//
// i?120 = ((((dist + time as f32 / 63 as f32) as i32 ^ angle as i32) #% 32 + 32) >> ((dist as i32 - i % 7 * 3) / 40)) + 192;
//
// branch_if (i := i + 1) < 320*240: pixels;
// }
//}
export fn tic(time: i32) {
let i: i32; let i: i32;
loop colors { loop colors {
rect((i % 16 * 15) as f32, (i / 16 * 15) as f32, 15 as f32, 15 as f32, i); rect((i % 16 * 15) as f32, (i / 16 * 15) as f32, 15 as f32, 15 as f32, i);
@@ -43,7 +22,7 @@ start fn gen_palette() {
let lazy a = max(llimit, min(ulimit, c)) * (scale + 0.05); let lazy a = max(llimit, min(ulimit, c)) * (scale + 0.05);
let lazy b = scale * scale * 0.8; let lazy b = scale * scale * 0.8;
let inline v = (select(i < 11*16*3, max(0 as f32, min(a + b - a * b, 1 as f32)), scale) * 255 as f32) as i32; let inline v = (select(i < 11*16*3, max(0 as f32, min(a + b - a * b, 1 as f32)), scale) * 255 as f32) as i32;
(i%3 + i/3*4)?(120+320*240) = v; (i%3 + i/3*4)?(PALETTE) = v;
avg = (avg + c) * 0.5; avg = (avg + c) * 0.5;
branch_if i := i - 1: gradients; branch_if i := i - 1: gradients;
@@ -56,15 +35,15 @@ start fn gen_palette() {
let lazy first_step = index >= 32; let lazy first_step = index >= 32;
let inline src1 = select(first_step, index % 32 / 2, index * 2); let inline src1 = select(first_step, index % 32 / 2, index * 2);
let inline src2 = select(first_step, (index + 1) % 32 / 2, index * 2 + 1); let inline src2 = select(first_step, (index + 1) % 32 / 2, index * 2 + 1);
let inline c1 = (src1 * 4 + channel)?(120+320*240+192*4); let inline c1 = (src1 * 4 + channel)?SWEETY;
let inline c2 = (src2 * 4 + channel)?(120+320*240+192*4); let inline c2 = (src2 * 4 + channel)?SWEETY;
i?(120+320*240+192*4) = (c1 + c2) * (3 + first_step) / 8; i?(SWEETY) = (c1 + c2) * (3 + first_step) / 8;
branch_if (i := i - 1) >= 0: expand_sweetie; branch_if (i := i - 1) >= 0: expand_sweetie;
} }
} }
data 120+320*240+192*4 { data SWEETY {
i32( i32(
0x2c1c1a, 0x2c1c1a,
0x5d275d, 0x5d275d,

13
examples/microw8/uw8.cwa Normal file
View File

@@ -0,0 +1,13 @@
import "env.memory" memory(4);
import "env.pow" fn pow(f32, f32) -> f32;
import "env.sin" fn sin(f32) -> f32;
import "env.cos" fn cos(f32) -> f32;
import "env.atan2" fn atan2(f32, f32) -> f32;
import "env.tan" fn tan(f32) -> f32;
import "env.atan" fn atan(f32) -> f32;
import "env.rectangle" fn rect(f32, f32, f32, f32, i32);
const FRAMEBUFFER = 120;
const PALETTE = 0x13000;
const FONT = 0x13400;

View File

@@ -8,7 +8,8 @@ pub struct Script {
pub global_vars: Vec<GlobalVar>, pub global_vars: Vec<GlobalVar>,
pub functions: Vec<Function>, pub functions: Vec<Function>,
pub data: Vec<Data>, pub data: Vec<Data>,
pub includes: Vec<Include> pub includes: Vec<Include>,
pub consts: Vec<GlobalConst>,
} }
impl Script { impl Script {
@@ -17,6 +18,7 @@ impl Script {
self.global_vars.append(&mut other.global_vars); self.global_vars.append(&mut other.global_vars);
self.functions.append(&mut other.functions); self.functions.append(&mut other.functions);
self.data.append(&mut other.data); self.data.append(&mut other.data);
self.consts.append(&mut other.consts);
assert!(other.includes.is_empty()); assert!(other.includes.is_empty());
} }
} }
@@ -27,7 +29,8 @@ pub enum TopLevelItem {
GlobalVar(GlobalVar), GlobalVar(GlobalVar),
Function(Function), Function(Function),
Data(Data), Data(Data),
Include(Include) Include(Include),
Const(GlobalConst),
} }
#[derive(Debug)] #[derive(Debug)]
@@ -67,6 +70,14 @@ pub struct GlobalVar {
pub mutable: bool, pub mutable: bool,
} }
#[derive(Debug)]
pub struct GlobalConst {
pub span: Span,
pub name: String,
pub value: Expression,
pub type_: Option<Type>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct Function { pub struct Function {
pub span: Span, pub span: Span,
@@ -170,7 +181,7 @@ pub enum DataType {
F64, F64,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct MemoryLocation { pub struct MemoryLocation {
pub span: Span, pub span: Span,
pub size: MemSize, pub size: MemSize,
@@ -178,7 +189,7 @@ pub struct MemoryLocation {
pub right: Box<Expression>, pub right: Box<Expression>,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Expression { pub struct Expression {
pub type_: Option<Type>, pub type_: Option<Type>,
pub expr: Expr, pub expr: Expr,
@@ -213,9 +224,16 @@ impl Expression {
_ => panic!("Expected F64Const"), _ => panic!("Expected F64Const"),
} }
} }
pub fn is_const(&self) -> bool {
match self.expr {
Expr::I32Const(_) | Expr::I64Const(_) | Expr::F32Const(_) | Expr::F64Const(_) => true,
_ => false,
}
}
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum Expr { pub enum Expr {
Block { Block {
statements: Vec<Expression>, statements: Vec<Expression>,
@@ -355,7 +373,7 @@ pub enum BinOp {
pub enum MemSize { pub enum MemSize {
Byte, Byte,
Word, Word,
Float Float,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]

View File

@@ -1,35 +1,103 @@
use crate::ast; use std::collections::HashMap;
use crate::{
ast,
parser::{Sources, Span},
typecheck::{report_duplicate_definition, report_error},
};
type Result<T> = std::result::Result<T, ()>;
pub fn fold_script(script: &mut ast::Script, sources: &Sources) -> Result<()> {
let mut context = Context {
consts: HashMap::new(),
sources,
};
fold_consts(&mut context, &mut script.consts)?;
pub fn fold_script(script: &mut ast::Script) {
for var in &mut script.global_vars { for var in &mut script.global_vars {
fold_expr(&mut var.value); fold_expr(&context, &mut var.value);
} }
for func in &mut script.functions { for func in &mut script.functions {
fold_expr(&mut func.body); fold_expr(&context, &mut func.body);
} }
for data in &mut script.data { for data in &mut script.data {
fold_expr(&mut data.offset); fold_expr(&context, &mut data.offset);
for values in &mut data.data { for values in &mut data.data {
match values { match values {
ast::DataValues::Array { values, .. } => { ast::DataValues::Array { values, .. } => {
for value in values { for value in values {
fold_expr(value); fold_expr(&context, value);
} }
} }
ast::DataValues::String(_) | ast::DataValues::File { .. } => (), ast::DataValues::String(_) | ast::DataValues::File { .. } => (),
} }
} }
} }
Ok(())
} }
fn fold_mem_location(mem_location: &mut ast::MemoryLocation) { struct Context<'a> {
fold_expr(&mut mem_location.left); consts: HashMap<String, ast::Expr>,
fold_expr(&mut mem_location.right); sources: &'a Sources,
} }
fn fold_expr(expr: &mut ast::Expression) { fn fold_consts(context: &mut Context, consts: &mut [ast::GlobalConst]) -> Result<()> {
let mut spans: HashMap<&str, Span> = HashMap::new();
for cnst in consts.iter_mut() {
if let Some(prev_span) = spans.insert(&cnst.name, cnst.span.clone()) {
report_duplicate_definition(
"Const already defined",
&cnst.span,
&prev_span,
context.sources,
)?;
}
}
while context.consts.len() < consts.len() {
let mut making_progress = false;
for cnst in consts.iter_mut() {
if !context.consts.contains_key(&cnst.name) {
fold_expr(context, &mut cnst.value);
if cnst.value.is_const() {
context
.consts
.insert(cnst.name.clone(), cnst.value.expr.clone());
making_progress = true;
}
}
}
if !making_progress {
break;
}
}
let mut result = Ok(());
for cnst in consts {
if !context.consts.contains_key(&cnst.name) {
result = report_error(
&format!("Failed to fold const '{}'", cnst.name),
&cnst.span,
context.sources,
);
}
}
result
}
fn fold_mem_location(context: &Context, mem_location: &mut ast::MemoryLocation) {
fold_expr(context, &mut mem_location.left);
fold_expr(context, &mut mem_location.right);
}
fn fold_expr(context: &Context, expr: &mut ast::Expression) {
use ast::BinOp::*; use ast::BinOp::*;
match expr.expr { match expr.expr {
ast::Expr::Block { ast::Expr::Block {
@@ -37,15 +105,15 @@ fn fold_expr(expr: &mut ast::Expression) {
ref mut final_expression, ref mut final_expression,
} => { } => {
for stmt in statements { for stmt in statements {
fold_expr(stmt); fold_expr(context, stmt);
} }
if let Some(ref mut expr) = final_expression { if let Some(ref mut expr) = final_expression {
fold_expr(expr); fold_expr(context, expr);
} }
} }
ast::Expr::Let { ref mut value, .. } => { ast::Expr::Let { ref mut value, .. } => {
if let Some(ref mut expr) = value { if let Some(ref mut expr) = value {
fold_expr(expr); fold_expr(context, expr);
} }
} }
ast::Expr::Poke { ast::Expr::Poke {
@@ -53,12 +121,12 @@ fn fold_expr(expr: &mut ast::Expression) {
ref mut value, ref mut value,
.. ..
} => { } => {
fold_mem_location(mem_location); fold_mem_location(context, mem_location);
fold_expr(value); fold_expr(context, value);
} }
ast::Expr::Peek(ref mut mem_location) => fold_mem_location(mem_location), ast::Expr::Peek(ref mut mem_location) => fold_mem_location(context, mem_location),
ast::Expr::UnaryOp { op, ref mut value } => { ast::Expr::UnaryOp { op, ref mut value } => {
fold_expr(value); fold_expr(context, value);
let result = match (op, &value.expr) { let result = match (op, &value.expr) {
(ast::UnaryOp::Negate, ast::Expr::I32Const(value)) => { (ast::UnaryOp::Negate, ast::Expr::I32Const(value)) => {
Some(ast::Expr::I32Const(-*value)) Some(ast::Expr::I32Const(-*value))
@@ -90,8 +158,8 @@ fn fold_expr(expr: &mut ast::Expression) {
ref mut right, ref mut right,
.. ..
} => { } => {
fold_expr(left); fold_expr(context, left);
fold_expr(right); fold_expr(context, right);
match (&left.expr, &right.expr) { match (&left.expr, &right.expr) {
(&ast::Expr::I32Const(left), &ast::Expr::I32Const(right)) => { (&ast::Expr::I32Const(left), &ast::Expr::I32Const(right)) => {
let result = match op { let result = match op {
@@ -237,24 +305,28 @@ fn fold_expr(expr: &mut ast::Expression) {
ast::Expr::I32Const(_) ast::Expr::I32Const(_)
| ast::Expr::I64Const(_) | ast::Expr::I64Const(_)
| ast::Expr::F32Const(_) | ast::Expr::F32Const(_)
| ast::Expr::F64Const(_) | ast::Expr::F64Const(_) => (),
| ast::Expr::Variable { .. } => (), ast::Expr::Variable { ref name, .. } => {
ast::Expr::Assign { ref mut value, .. } => fold_expr(value), if let Some(value) = context.consts.get(name) {
ast::Expr::LocalTee { ref mut value, .. } => fold_expr(value), expr.expr = value.clone();
ast::Expr::Loop { ref mut block, .. } => fold_expr(block), }
ast::Expr::LabelBlock { ref mut block, .. } => fold_expr(block), }
ast::Expr::Assign { ref mut value, .. } => fold_expr(context, value),
ast::Expr::LocalTee { ref mut value, .. } => fold_expr(context, value),
ast::Expr::Loop { ref mut block, .. } => fold_expr(context, block),
ast::Expr::LabelBlock { ref mut block, .. } => fold_expr(context, block),
ast::Expr::Branch(_) => (), ast::Expr::Branch(_) => (),
ast::Expr::BranchIf { ast::Expr::BranchIf {
ref mut condition, .. ref mut condition, ..
} => fold_expr(condition), } => fold_expr(context, condition),
ast::Expr::Cast { ref mut value, .. } => fold_expr(value), ast::Expr::Cast { ref mut value, .. } => fold_expr(context, value),
ast::Expr::FuncCall { ast::Expr::FuncCall {
ref name, ref name,
ref mut params, ref mut params,
.. ..
} => { } => {
for param in params.iter_mut() { for param in params.iter_mut() {
fold_expr(param); fold_expr(context, param);
} }
use ast::Expr::*; use ast::Expr::*;
let params: Vec<_> = params.iter().map(|e| &e.expr).collect(); let params: Vec<_> = params.iter().map(|e| &e.expr).collect();
@@ -269,31 +341,31 @@ fn fold_expr(expr: &mut ast::Expression) {
ref mut if_false, ref mut if_false,
.. ..
} => { } => {
fold_expr(condition); fold_expr(context, condition);
fold_expr(if_true); fold_expr(context, if_true);
fold_expr(if_false); fold_expr(context, if_false);
} }
ast::Expr::If { ast::Expr::If {
ref mut condition, ref mut condition,
ref mut if_true, ref mut if_true,
ref mut if_false, ref mut if_false,
} => { } => {
fold_expr(condition); fold_expr(context, condition);
fold_expr(if_true); fold_expr(context, if_true);
if let Some(ref mut if_false) = if_false { if let Some(ref mut if_false) = if_false {
fold_expr(if_false); fold_expr(context, if_false);
} }
} }
ast::Expr::Return { ast::Expr::Return {
value: Some(ref mut value), value: Some(ref mut value),
} => fold_expr(value), } => fold_expr(context, value),
ast::Expr::Return { value: None } => (), ast::Expr::Return { value: None } => (),
ast::Expr::First { ast::Expr::First {
ref mut value, ref mut value,
ref mut drop, ref mut drop,
} => { } => {
fold_expr(value); fold_expr(context, value);
fold_expr(drop); fold_expr(context, drop);
} }
ast::Expr::Error => unreachable!(), ast::Expr::Error => unreachable!(),
} }

View File

@@ -1,7 +1,7 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use parser::Sources;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
use parser::Sources;
mod ast; mod ast;
mod constfold; mod constfold;
@@ -36,15 +36,18 @@ pub fn compile_file<P: AsRef<Path>>(path: P, options: Options) -> Result<Vec<u8>
Ok(script) => script, Ok(script) => script,
Err(_) => bail!("Parse failed"), Err(_) => bail!("Parse failed"),
}; };
includes::resolve_includes(&mut new_script, &path)?; includes::resolve_includes(&mut new_script, &path)?;
for include in std::mem::take(&mut new_script.includes) { for include in std::mem::take(&mut new_script.includes) {
let mut path = path.parent().expect("Script path has no parent").to_path_buf(); let mut path = path
.parent()
.expect("Script path has no parent")
.to_path_buf();
path.push(include.path); path.push(include.path);
pending_files.push((path, Some(include.span))); pending_files.push((path, Some(include.span)));
} }
script.merge(new_script); script.merge(new_script);
} }
Ok((_, false)) => (), // already parsed this include Ok((_, false)) => (), // already parsed this include
@@ -59,8 +62,9 @@ pub fn compile_file<P: AsRef<Path>>(path: P, options: Options) -> Result<Vec<u8>
} }
} }
if constfold::fold_script(&mut script, &sources).is_err() {
constfold::fold_script(&mut script); bail!("Constant folding failed");
}
if typecheck::tc_script(&mut script, &sources).is_err() { if typecheck::tc_script(&mut script, &sources).is_err() {
bail!("Type check failed"); bail!("Type check failed");
} }

View File

@@ -925,6 +925,21 @@ fn script_parser() -> impl Parser<Token, ast::Script, Error = ScriptError> + Clo
}) })
.boxed(); .boxed();
let global_const = just(Token::Ident("const".to_string()))
.ignore_then(identifier)
.then(just(Token::Ctrl(':')).ignore_then(type_parser()).or_not())
.then(just(Token::Op("=".to_string())).ignore_then(expression.clone()))
.then_ignore(just(Token::Ctrl(';')))
.map_with_span(|((name, type_), value), span| {
ast::TopLevelItem::Const(ast::GlobalConst {
name,
type_,
value,
span,
})
})
.boxed();
let data_i8 = just(Token::Ident("i8".to_string())) let data_i8 = just(Token::Ident("i8".to_string()))
.to(ast::DataType::I8) .to(ast::DataType::I8)
.or(just(Token::Ident("i16".to_string())).to(ast::DataType::I16)) .or(just(Token::Ident("i16".to_string())).to(ast::DataType::I16))
@@ -975,17 +990,17 @@ fn script_parser() -> impl Parser<Token, ast::Script, Error = ScriptError> + Clo
|path, span| ast::TopLevelItem::Include(ast::Include { span, path }), |path, span| ast::TopLevelItem::Include(ast::Include { span, path }),
)); ));
import.or(function).or(global).or(data).or(include).boxed() import
.or(function)
.or(global)
.or(data)
.or(include)
.or(global_const)
.boxed()
}; };
top_level_item.repeated().then_ignore(end()).map(|items| { top_level_item.repeated().then_ignore(end()).map(|items| {
let mut script = ast::Script { let mut script = ast::Script::default();
imports: Vec::new(),
global_vars: Vec::new(),
functions: Vec::new(),
data: Vec::new(),
includes: Vec::new(),
};
for item in items { for item in items {
match item { match item {
ast::TopLevelItem::Import(i) => script.imports.push(i), ast::TopLevelItem::Import(i) => script.imports.push(i),
@@ -993,6 +1008,7 @@ fn script_parser() -> impl Parser<Token, ast::Script, Error = ScriptError> + Clo
ast::TopLevelItem::Function(f) => script.functions.push(f), ast::TopLevelItem::Function(f) => script.functions.push(f),
ast::TopLevelItem::Data(d) => script.data.push(d), ast::TopLevelItem::Data(d) => script.data.push(d),
ast::TopLevelItem::Include(i) => script.includes.push(i), ast::TopLevelItem::Include(i) => script.includes.push(i),
ast::TopLevelItem::Const(c) => script.consts.push(c),
} }
} }
script script

View File

@@ -104,6 +104,17 @@ pub fn tc_script(script: &mut ast::Script, sources: &Sources) -> Result<()> {
} }
} }
for c in &mut script.consts {
tc_const(&mut c.value, sources)?;
if c.value.type_ != c.type_ {
if c.type_.is_some() {
result = type_mismatch(c.type_, &c.span, c.value.type_, &c.value.span, sources);
} else {
c.type_ = c.value.type_;
}
}
}
for f in &script.functions { for f in &script.functions {
let params = f.params.iter().map(|(_, t)| *t).collect(); let params = f.params.iter().map(|(_, t)| *t).collect();
if let Some(fnc) = context.functions.get(&f.name) { if let Some(fnc) = context.functions.get(&f.name) {
@@ -298,7 +309,7 @@ impl LocalVars {
} }
} }
fn report_duplicate_definition( pub fn report_duplicate_definition(
msg: &str, msg: &str,
span: &Span, span: &Span,
prev_span: &Span, prev_span: &Span,