mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-30 23:31:12 +00:00
Merge remote-tracking branch 'origin/trunk' into tail-calls
This commit is contained in:
commit
ad23fc7e4b
6 changed files with 9 additions and 1490 deletions
File diff suppressed because it is too large
Load diff
|
@ -1,52 +0,0 @@
|
||||||
use cranelift::prelude::AbiParam;
|
|
||||||
use cranelift_codegen::ir::{types, Signature, Type};
|
|
||||||
use cranelift_codegen::isa::TargetFrontendConfig;
|
|
||||||
use cranelift_module::{Backend, Module};
|
|
||||||
|
|
||||||
use roc_mono::layout::Layout;
|
|
||||||
|
|
||||||
pub fn type_from_layout(cfg: TargetFrontendConfig, layout: &Layout<'_>) -> Type {
|
|
||||||
use roc_mono::layout::Builtin::*;
|
|
||||||
use roc_mono::layout::Layout::*;
|
|
||||||
|
|
||||||
match layout {
|
|
||||||
FunctionPointer(_, _) | Pointer(_) | Struct(_) | Union(_) => cfg.pointer_type(),
|
|
||||||
Builtin(builtin) => match builtin {
|
|
||||||
Int64 => types::I64,
|
|
||||||
Float64 => types::F64,
|
|
||||||
Bool => types::I8,
|
|
||||||
Byte => types::I8,
|
|
||||||
Str | EmptyStr | Map(_, _) | EmptyMap | Set(_) | EmptySet | List(_) | EmptyList => {
|
|
||||||
cfg.pointer_type()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sig_from_layout<B: Backend>(
|
|
||||||
cfg: TargetFrontendConfig,
|
|
||||||
module: &mut Module<B>,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
) -> Signature {
|
|
||||||
match layout {
|
|
||||||
Layout::FunctionPointer(args, ret) => {
|
|
||||||
let ret_type = type_from_layout(cfg, &ret);
|
|
||||||
let mut sig = module.make_signature();
|
|
||||||
|
|
||||||
// Add return type to the signature
|
|
||||||
sig.returns.push(AbiParam::new(ret_type));
|
|
||||||
|
|
||||||
// Add params to the signature
|
|
||||||
for layout in args.iter() {
|
|
||||||
let arg_type = type_from_layout(cfg, &layout);
|
|
||||||
|
|
||||||
sig.params.push(AbiParam::new(arg_type));
|
|
||||||
}
|
|
||||||
|
|
||||||
sig
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
panic!("Could not make Signature from Layout {:?}", layout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
use cranelift::prelude::{AbiParam, FunctionBuilder, FunctionBuilderContext};
|
|
||||||
use cranelift_codegen::ir::{ExternalName, Function, InstBuilder, Signature};
|
|
||||||
use cranelift_codegen::isa::CallConv;
|
|
||||||
use cranelift_codegen::Context;
|
|
||||||
use cranelift_module::{Backend, FuncId, Linkage, Module};
|
|
||||||
|
|
||||||
pub fn declare_malloc_header<B: Backend>(module: &mut Module<B>) -> (FuncId, Signature) {
|
|
||||||
let ptr_size_type = module.target_config().pointer_type();
|
|
||||||
let sig = Signature {
|
|
||||||
params: vec![AbiParam::new(ptr_size_type)],
|
|
||||||
returns: vec![AbiParam::new(ptr_size_type)],
|
|
||||||
call_conv: CallConv::SystemV, // TODO is this the calling convention we actually want?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Declare the wrapper around malloc
|
|
||||||
let func_id = module
|
|
||||||
.declare_function("roc_malloc", Linkage::Local, &sig)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
(func_id, sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn define_malloc_body<B: Backend>(
|
|
||||||
module: &mut Module<B>,
|
|
||||||
ctx: &mut Context,
|
|
||||||
sig: Signature,
|
|
||||||
func_id: FuncId,
|
|
||||||
) {
|
|
||||||
let ptr_size_type = module.target_config().pointer_type();
|
|
||||||
|
|
||||||
ctx.func = Function::with_name_signature(ExternalName::user(0, func_id.as_u32()), sig);
|
|
||||||
|
|
||||||
let mut func_ctx = FunctionBuilderContext::new();
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut builder: FunctionBuilder = FunctionBuilder::new(&mut ctx.func, &mut func_ctx);
|
|
||||||
let block = builder.create_block();
|
|
||||||
|
|
||||||
builder.switch_to_block(block);
|
|
||||||
builder.append_block_params_for_function_params(block);
|
|
||||||
|
|
||||||
let mut malloc_sig = module.make_signature();
|
|
||||||
|
|
||||||
malloc_sig.params.push(AbiParam::new(ptr_size_type));
|
|
||||||
malloc_sig.returns.push(AbiParam::new(ptr_size_type));
|
|
||||||
|
|
||||||
let callee = module
|
|
||||||
.declare_function("malloc", Linkage::Import, &malloc_sig)
|
|
||||||
.expect("declare malloc");
|
|
||||||
let malloc = module.declare_func_in_func(callee, &mut builder.func);
|
|
||||||
let size = builder.block_params(block)[0];
|
|
||||||
let call = builder.ins().call(malloc, &[size]);
|
|
||||||
let ptr = builder.inst_results(call)[0];
|
|
||||||
|
|
||||||
builder.ins().return_(&[ptr]);
|
|
||||||
|
|
||||||
// TODO re-enable this once Switch stops making unsealed blocks, e.g.
|
|
||||||
// https://docs.rs/cranelift-frontend/0.59.0/src/cranelift_frontend/switch.rs.html#152
|
|
||||||
// builder.seal_block(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.define_function(func_id, ctx).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn define_malloc<B: Backend>(module: &mut Module<B>, ctx: &mut Context) -> FuncId {
|
|
||||||
// TODO investigate whether we can remove this wrapper function somehow.
|
|
||||||
// It may get inlined away, but it seems like it shouldn't be
|
|
||||||
// necessary, and we should be able to return the FuncId of the imported malloc.
|
|
||||||
let ptr_size_type = module.target_config().pointer_type();
|
|
||||||
let sig = Signature {
|
|
||||||
params: vec![AbiParam::new(ptr_size_type)],
|
|
||||||
returns: vec![AbiParam::new(ptr_size_type)],
|
|
||||||
call_conv: CallConv::SystemV, // TODO is this the calling convention we actually want?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Declare the wrapper around malloc
|
|
||||||
let func_id = module
|
|
||||||
.declare_function("roc_malloc", Linkage::Local, &sig)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let ptr_size_type = module.target_config().pointer_type();
|
|
||||||
|
|
||||||
ctx.func = Function::with_name_signature(ExternalName::user(0, func_id.as_u32()), sig);
|
|
||||||
|
|
||||||
let mut func_ctx = FunctionBuilderContext::new();
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut builder: FunctionBuilder = FunctionBuilder::new(&mut ctx.func, &mut func_ctx);
|
|
||||||
let block = builder.create_block();
|
|
||||||
|
|
||||||
builder.switch_to_block(block);
|
|
||||||
builder.append_block_params_for_function_params(block);
|
|
||||||
|
|
||||||
let mut malloc_sig = module.make_signature();
|
|
||||||
|
|
||||||
malloc_sig.params.push(AbiParam::new(ptr_size_type));
|
|
||||||
malloc_sig.returns.push(AbiParam::new(ptr_size_type));
|
|
||||||
|
|
||||||
let callee = module
|
|
||||||
.declare_function("malloc", Linkage::Import, &malloc_sig)
|
|
||||||
.expect("declare malloc");
|
|
||||||
let malloc = module.declare_func_in_func(callee, &mut builder.func);
|
|
||||||
let size = builder.block_params(block)[0];
|
|
||||||
let call = builder.ins().call(malloc, &[size]);
|
|
||||||
let ptr = builder.inst_results(call)[0];
|
|
||||||
|
|
||||||
builder.ins().return_(&[ptr]);
|
|
||||||
|
|
||||||
builder.seal_block(block);
|
|
||||||
builder.finalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.define_function(func_id, ctx).unwrap();
|
|
||||||
module.clear_context(ctx);
|
|
||||||
|
|
||||||
func_id
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod build;
|
|
||||||
pub mod convert;
|
|
||||||
pub mod imports;
|
|
|
@ -10,5 +10,5 @@
|
||||||
// and encouraging shortcuts here creates bad incentives. I would rather temporarily
|
// and encouraging shortcuts here creates bad incentives. I would rather temporarily
|
||||||
// re-enable this when working on performance optimizations than have it block PRs.
|
// re-enable this when working on performance optimizations than have it block PRs.
|
||||||
#![allow(clippy::large_enum_variant)]
|
#![allow(clippy::large_enum_variant)]
|
||||||
pub mod crane;
|
|
||||||
pub mod llvm;
|
pub mod llvm;
|
||||||
|
|
|
@ -14,181 +14,23 @@ mod helpers;
|
||||||
mod test_gen {
|
mod test_gen {
|
||||||
use crate::helpers::{can_expr, infer_expr, uniq_expr, CanExprOut};
|
use crate::helpers::{can_expr, infer_expr, uniq_expr, CanExprOut};
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use cranelift::prelude::{AbiParam, ExternalName, FunctionBuilder, FunctionBuilderContext};
|
|
||||||
use cranelift_codegen::ir::{ArgumentPurpose, InstBuilder};
|
|
||||||
use cranelift_codegen::settings;
|
|
||||||
use cranelift_codegen::verifier::verify_function;
|
|
||||||
use cranelift_module::{default_libcall_names, Linkage, Module};
|
|
||||||
use cranelift_simplejit::{SimpleJITBackend, SimpleJITBuilder};
|
|
||||||
use inkwell::context::Context;
|
use inkwell::context::Context;
|
||||||
use inkwell::execution_engine::JitFunction;
|
use inkwell::execution_engine::JitFunction;
|
||||||
use inkwell::passes::PassManager;
|
use inkwell::passes::PassManager;
|
||||||
use inkwell::types::BasicType;
|
use inkwell::types::BasicType;
|
||||||
use inkwell::OptimizationLevel;
|
use inkwell::OptimizationLevel;
|
||||||
use roc_collections::all::ImMap;
|
use roc_collections::all::ImMap;
|
||||||
use roc_gen::crane::build::{declare_proc, define_proc_body, ScopeEntry};
|
|
||||||
use roc_gen::crane::convert::type_from_layout;
|
|
||||||
use roc_gen::crane::imports::define_malloc;
|
|
||||||
use roc_gen::llvm::build::{build_proc, build_proc_header};
|
use roc_gen::llvm::build::{build_proc, build_proc_header};
|
||||||
use roc_gen::llvm::convert::basic_type_from_layout;
|
use roc_gen::llvm::convert::basic_type_from_layout;
|
||||||
use roc_mono::expr::{Expr, Procs};
|
use roc_mono::expr::{Expr, Procs};
|
||||||
use roc_mono::layout::Layout;
|
use roc_mono::layout::Layout;
|
||||||
use roc_types::subs::Subs;
|
use roc_types::subs::Subs;
|
||||||
use std::ffi::{CStr, CString};
|
use std::ffi::{CStr, CString};
|
||||||
use std::mem;
|
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
|
|
||||||
// Pointer size on 64-bit platforms
|
// Pointer size on 64-bit platforms
|
||||||
const POINTER_SIZE: u32 = std::mem::size_of::<u64>() as u32;
|
const POINTER_SIZE: u32 = std::mem::size_of::<u64>() as u32;
|
||||||
|
|
||||||
macro_rules! assert_crane_evals_to {
|
|
||||||
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
|
|
||||||
let arena = Bump::new();
|
|
||||||
let CanExprOut { loc_expr, var_store, var, constraint, home, interns, .. } = can_expr($src);
|
|
||||||
let subs = Subs::new(var_store.into());
|
|
||||||
let mut unify_problems = Vec::new();
|
|
||||||
let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var);
|
|
||||||
let shared_builder = settings::builder();
|
|
||||||
let shared_flags = settings::Flags::new(shared_builder);
|
|
||||||
let mut module: Module<SimpleJITBackend> =
|
|
||||||
Module::new(SimpleJITBuilder::new(default_libcall_names()));
|
|
||||||
|
|
||||||
let cfg = module.target_config();
|
|
||||||
let mut ctx = module.make_context();
|
|
||||||
let malloc = define_malloc(&mut module, &mut ctx);
|
|
||||||
let mut func_ctx = FunctionBuilderContext::new();
|
|
||||||
|
|
||||||
let main_fn_name = "$Test.main";
|
|
||||||
|
|
||||||
// Compute main_fn_ret_type before moving subs to Env
|
|
||||||
let layout = Layout::from_content(&arena, content, &subs, POINTER_SIZE)
|
|
||||||
.unwrap_or_else(|err| panic!("Code gen error in test: could not convert content to layout. Err was {:?} and Subs were {:?}", err, subs));
|
|
||||||
|
|
||||||
// Compile and add all the Procs before adding main
|
|
||||||
let mut procs = Procs::default();
|
|
||||||
let mut env = roc_gen::crane::build::Env {
|
|
||||||
arena: &arena,
|
|
||||||
interns,
|
|
||||||
cfg,
|
|
||||||
malloc,
|
|
||||||
variable_counter: &mut 0
|
|
||||||
|
|
||||||
};
|
|
||||||
let mut ident_ids = env.interns.all_ident_ids.remove(&home).unwrap();
|
|
||||||
|
|
||||||
// Populate Procs and Subs, and get the low-level Expr from the canonical Expr
|
|
||||||
let mono_expr = Expr::new(&arena, &mut subs, loc_expr.value, &mut procs, home, &mut ident_ids, POINTER_SIZE);
|
|
||||||
|
|
||||||
// Put this module's ident_ids back in the interns
|
|
||||||
env.interns.all_ident_ids.insert(home, ident_ids);
|
|
||||||
|
|
||||||
let mut scope = ImMap::default();
|
|
||||||
let mut declared = Vec::with_capacity(procs.len());
|
|
||||||
|
|
||||||
// Declare all the Procs, then insert them into scope so their bodies
|
|
||||||
// can look up their Funcs in scope later when calling each other by value.
|
|
||||||
for (name, opt_proc) in procs.as_map().into_iter() {
|
|
||||||
if let Some(proc) = opt_proc {
|
|
||||||
let (func_id, sig) = declare_proc(&mut env, &mut module, name, &proc);
|
|
||||||
|
|
||||||
declared.push((proc.clone(), sig.clone(), func_id));
|
|
||||||
|
|
||||||
scope.insert(name.clone(), ScopeEntry::Func { func_id, sig });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (proc, sig, fn_id) in declared {
|
|
||||||
define_proc_body(
|
|
||||||
&mut env,
|
|
||||||
&mut ctx,
|
|
||||||
&mut module,
|
|
||||||
fn_id,
|
|
||||||
&scope,
|
|
||||||
sig,
|
|
||||||
arena.alloc(proc),
|
|
||||||
&procs,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify the function we just defined
|
|
||||||
if let Err(errors) = verify_function(&ctx.func, &shared_flags) {
|
|
||||||
// NOTE: We don't include proc here because it's already
|
|
||||||
// been moved. If you need to know which proc failed, go back
|
|
||||||
// and add some logging.
|
|
||||||
panic!("Errors defining proc: {}", errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add main itself
|
|
||||||
let mut sig = module.make_signature();
|
|
||||||
|
|
||||||
// Add return type to the signature.
|
|
||||||
// If it is a struct, give it a special return type.
|
|
||||||
// Otherwise, Cranelift will return a raw pointer to the struct
|
|
||||||
// instead of using a proper struct return.
|
|
||||||
match layout {
|
|
||||||
Layout::Struct(fields) => {
|
|
||||||
for field_layout in fields {
|
|
||||||
let ret_type = type_from_layout(cfg, &field_layout);
|
|
||||||
let abi_param = AbiParam::special(ret_type, ArgumentPurpose::StructReturn);
|
|
||||||
|
|
||||||
sig.returns.push(abi_param);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
let main_ret_type = type_from_layout(cfg, &layout);
|
|
||||||
|
|
||||||
sig.returns.push(AbiParam::new(main_ret_type));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let main_fn = module
|
|
||||||
.declare_function(main_fn_name, Linkage::Local, &sig)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ctx.func.signature = sig;
|
|
||||||
ctx.func.name = ExternalName::user(0, main_fn.as_u32());
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut builder: FunctionBuilder =
|
|
||||||
FunctionBuilder::new(&mut ctx.func, &mut func_ctx);
|
|
||||||
let block = builder.create_block();
|
|
||||||
|
|
||||||
builder.switch_to_block(block);
|
|
||||||
// TODO try deleting this line and seeing if everything still works.
|
|
||||||
builder.append_block_params_for_function_params(block);
|
|
||||||
|
|
||||||
let main_body =
|
|
||||||
roc_gen::crane::build::build_expr(&mut env, &scope, &mut module, &mut builder, &mono_expr, &procs);
|
|
||||||
|
|
||||||
builder.ins().return_(&[main_body]);
|
|
||||||
// TODO re-enable this once Switch stops making unsealed blocks, e.g.
|
|
||||||
// https://docs.rs/cranelift-frontend/0.59.0/src/cranelift_frontend/switch.rs.html#152
|
|
||||||
// builder.seal_block(block);
|
|
||||||
builder.seal_all_blocks();
|
|
||||||
builder.finalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.define_function(main_fn, &mut ctx).expect("crane declare main");
|
|
||||||
module.clear_context(&mut ctx);
|
|
||||||
|
|
||||||
// Perform linking
|
|
||||||
module.finalize_definitions();
|
|
||||||
|
|
||||||
// Verify the main function
|
|
||||||
if let Err(errors) = verify_function(&ctx.func, &shared_flags) {
|
|
||||||
panic!("Errors defining {} - {}", main_fn_name, errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
let main_ptr = module.get_finalized_function(main_fn);
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let run_main = mem::transmute::<_, fn() -> $ty>(main_ptr) ;
|
|
||||||
|
|
||||||
assert_eq!($transform(run_main()), $expected);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! assert_llvm_evals_to {
|
macro_rules! assert_llvm_evals_to {
|
||||||
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
|
($src:expr, $expected:expr, $ty:ty, $transform:expr) => {
|
||||||
let arena = Bump::new();
|
let arena = Bump::new();
|
||||||
|
@ -476,7 +318,7 @@ mod test_gen {
|
||||||
|
|
||||||
macro_rules! assert_evals_to {
|
macro_rules! assert_evals_to {
|
||||||
($src:expr, $expected:expr, $ty:ty) => {
|
($src:expr, $expected:expr, $ty:ty) => {
|
||||||
// Run Cranelift tests, then LLVM tests, in separate scopes.
|
// Run un-optimized tests, and then optimized tests, in separate scopes.
|
||||||
// These each rebuild everything from scratch, starting with
|
// These each rebuild everything from scratch, starting with
|
||||||
// parsing the source, so that there's no chance their passing
|
// parsing the source, so that there's no chance their passing
|
||||||
// or failing depends on leftover state from the previous one.
|
// or failing depends on leftover state from the previous one.
|
||||||
|
@ -568,12 +410,12 @@ mod test_gen {
|
||||||
//
|
//
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_list_literal() {
|
fn empty_list_literal() {
|
||||||
assert_opt_evals_to!("[]", &[], &'static [i64], |x| x);
|
assert_evals_to!("[]", &[], &'static [i64], |x| x);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn int_list_literal() {
|
fn int_list_literal() {
|
||||||
assert_opt_evals_to!("[ 12, 9, 6, 3 ]", &[12, 9, 6, 3], &'static [i64], |x| x);
|
assert_evals_to!("[ 12, 9, 6, 3 ]", &[12, 9, 6, 3], &'static [i64], |x| x);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -593,7 +435,7 @@ mod test_gen {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_unique_int_list() {
|
fn set_unique_int_list() {
|
||||||
assert_opt_evals_to!(
|
assert_evals_to!(
|
||||||
"List.set [ 12, 9, 7, 1, 5 ] 2 33",
|
"List.set [ 12, 9, 7, 1, 5 ] 2 33",
|
||||||
&[12, 9, 33, 1, 5],
|
&[12, 9, 33, 1, 5],
|
||||||
&'static [i64],
|
&'static [i64],
|
||||||
|
@ -603,7 +445,7 @@ mod test_gen {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_unique_list_oob() {
|
fn set_unique_list_oob() {
|
||||||
assert_opt_evals_to!(
|
assert_evals_to!(
|
||||||
"List.set [ 3, 17, 4.1 ] 1337 9.25",
|
"List.set [ 3, 17, 4.1 ] 1337 9.25",
|
||||||
&[3.0, 17.0, 4.1],
|
&[3.0, 17.0, 4.1],
|
||||||
&'static [f64],
|
&'static [f64],
|
||||||
|
@ -613,7 +455,7 @@ mod test_gen {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_shared_int_list() {
|
fn set_shared_int_list() {
|
||||||
assert_opt_evals_to!(
|
assert_evals_to!(
|
||||||
indoc!(
|
indoc!(
|
||||||
r#"
|
r#"
|
||||||
shared = [ 2.1, 4.3 ]
|
shared = [ 2.1, 4.3 ]
|
||||||
|
@ -632,7 +474,7 @@ mod test_gen {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_shared_list_oob() {
|
fn set_shared_list_oob() {
|
||||||
assert_opt_evals_to!(
|
assert_evals_to!(
|
||||||
indoc!(
|
indoc!(
|
||||||
r#"
|
r#"
|
||||||
shared = [ 2, 4 ]
|
shared = [ 2, 4 ]
|
||||||
|
@ -1934,7 +1776,7 @@ mod test_gen {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn i64_record_literal() {
|
fn i64_record_literal() {
|
||||||
assert_opt_evals_to!(
|
assert_evals_to!(
|
||||||
indoc!(
|
indoc!(
|
||||||
r#"
|
r#"
|
||||||
{ x: 3, y: 5 }
|
{ x: 3, y: 5 }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue