@@ -6,7 +6,7 @@ use std::{
|
|||||||
|
|
||||||
use decodal::{
|
use decodal::{
|
||||||
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
|
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
|
||||||
format_diagnostic_with, format_source_with_source_id,
|
format_diagnostic_with,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -38,17 +38,6 @@ fn run() -> Result<(), String> {
|
|||||||
println!("ok");
|
println!("ok");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"fmt" | "format" => {
|
|
||||||
let next = args.next().unwrap_or_else(|| String::from("-"));
|
|
||||||
if next == "--write" || next == "-w" {
|
|
||||||
let path = args
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| String::from("missing file path for `decodal fmt --write`"))?;
|
|
||||||
format_path(&path, true)
|
|
||||||
} else {
|
|
||||||
format_path(&next, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"eval" | "materialize" => {
|
"eval" | "materialize" => {
|
||||||
let path = args.next().unwrap_or_else(|| String::from("-"));
|
let path = args.next().unwrap_or_else(|| String::from("-"));
|
||||||
let data = materialize_path(&path)?;
|
let data = materialize_path(&path)?;
|
||||||
@@ -65,21 +54,6 @@ fn run() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_path(path: &str, write: bool) -> Result<(), String> {
|
|
||||||
if write && path == "-" {
|
|
||||||
return Err(String::from("cannot use --write with stdin"));
|
|
||||||
}
|
|
||||||
let root = read_root_source(path).map_err(format_raw_diagnostic)?;
|
|
||||||
let formatted = format_source_with_source_id(SourceId(0), &root.source)
|
|
||||||
.map_err(|error| format_diagnostic_with_root(&error, &root.name))?;
|
|
||||||
if write {
|
|
||||||
fs::write(path, formatted).map_err(|error| format!("failed to write `{path}`: {error}"))?;
|
|
||||||
} else {
|
|
||||||
print!("{formatted}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn materialize_path(path: &str) -> Result<Data, String> {
|
fn materialize_path(path: &str) -> Result<Data, String> {
|
||||||
let root = read_root_source(path).map_err(format_raw_diagnostic)?;
|
let root = read_root_source(path).map_err(format_raw_diagnostic)?;
|
||||||
let root_name = root.name.clone();
|
let root_name = root.name.clone();
|
||||||
@@ -172,7 +146,6 @@ fn print_help() {
|
|||||||
println!(" decodal <file.dcdl> Materialize a DCDL file");
|
println!(" decodal <file.dcdl> Materialize a DCDL file");
|
||||||
println!(" decodal eval <file.dcdl> Materialize a DCDL file");
|
println!(" decodal eval <file.dcdl> Materialize a DCDL file");
|
||||||
println!(" decodal check <file.dcdl> Evaluate and materialize without printing data");
|
println!(" decodal check <file.dcdl> Evaluate and materialize without printing data");
|
||||||
println!(" decodal fmt [--write] <file.dcdl> Format a DCDL file");
|
|
||||||
println!(" decodal - Read DCDL source from stdin");
|
println!(" decodal - Read DCDL source from stdin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,442 +0,0 @@
|
|||||||
use alloc::string::{String, ToString};
|
|
||||||
use core::fmt::Write;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Ast, Diagnostic, Expr, ExprId, Field, Literal, Param, SourceForm, Span,
|
|
||||||
ast::{BinaryOp, CompareOp, MatchArm, UnaryOp},
|
|
||||||
diagnostic::Result,
|
|
||||||
parse_source_with_source_id,
|
|
||||||
span::SourceId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const INDENT: usize = 4;
|
|
||||||
|
|
||||||
pub fn format_source(source: &str) -> Result<String> {
|
|
||||||
format_source_with_source_id(SourceId(0), source)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_source_with_source_id(source_id: SourceId, source: &str) -> Result<String> {
|
|
||||||
reject_comments(source_id, source)?;
|
|
||||||
let parsed = parse_source_with_source_id(source_id, source)?;
|
|
||||||
Ok(Formatter::new(&parsed.ast).format_root(parsed.root, parsed.source_form))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Formatter<'a> {
|
|
||||||
ast: &'a Ast,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Formatter<'a> {
|
|
||||||
fn new(ast: &'a Ast) -> Self {
|
|
||||||
Self { ast }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_root(&self, root: ExprId, source_form: SourceForm) -> String {
|
|
||||||
let mut out = String::new();
|
|
||||||
match source_form {
|
|
||||||
SourceForm::Fields => {
|
|
||||||
if let Expr::Object(fields) = &self.ast.get(root).expr {
|
|
||||||
self.write_fields(&mut out, fields, 0);
|
|
||||||
} else {
|
|
||||||
self.write_expr(&mut out, root, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SourceForm::Expr => self.write_expr(&mut out, root, 0, 0),
|
|
||||||
}
|
|
||||||
if !out.ends_with('\n') {
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_expr(&self, out: &mut String, id: ExprId, indent: usize, parent_prec: u8) {
|
|
||||||
let expr = &self.ast.get(id).expr;
|
|
||||||
let prec = expr_prec(expr);
|
|
||||||
let paren = prec < parent_prec;
|
|
||||||
if paren {
|
|
||||||
out.push('(');
|
|
||||||
}
|
|
||||||
match expr {
|
|
||||||
Expr::Literal(literal) => self.write_literal(out, literal),
|
|
||||||
Expr::Ident(name) => out.push_str(name),
|
|
||||||
Expr::Object(fields) => self.write_object(out, fields, indent),
|
|
||||||
Expr::Array(items) => self.write_array(out, items, indent),
|
|
||||||
Expr::Let { bindings, body } => self.write_let(out, bindings, *body, indent),
|
|
||||||
Expr::Import(path) => {
|
|
||||||
out.push_str("import ");
|
|
||||||
write_quoted(out, path);
|
|
||||||
}
|
|
||||||
Expr::Path { base, field } => {
|
|
||||||
self.write_expr(out, *base, indent, prec);
|
|
||||||
out.push('.');
|
|
||||||
out.push_str(field);
|
|
||||||
}
|
|
||||||
Expr::Call { callee, args } => {
|
|
||||||
self.write_expr(out, *callee, indent, prec);
|
|
||||||
out.push('(');
|
|
||||||
for (index, arg) in args.iter().enumerate() {
|
|
||||||
if index > 0 {
|
|
||||||
out.push_str(", ");
|
|
||||||
}
|
|
||||||
self.write_expr(out, *arg, indent, 0);
|
|
||||||
}
|
|
||||||
out.push(')');
|
|
||||||
}
|
|
||||||
Expr::Function { params, body } => self.write_function(out, params, *body, indent),
|
|
||||||
Expr::Match { scrutinee, arms } => self.write_match(out, *scrutinee, arms, indent),
|
|
||||||
Expr::Unary { op, expr } => {
|
|
||||||
out.push_str(match op {
|
|
||||||
UnaryOp::Neg => "-",
|
|
||||||
UnaryOp::Not => "!",
|
|
||||||
});
|
|
||||||
self.write_expr(out, *expr, indent, prec);
|
|
||||||
}
|
|
||||||
Expr::Binary { op, lhs, rhs } => {
|
|
||||||
self.write_expr(out, *lhs, indent, prec);
|
|
||||||
out.push(' ');
|
|
||||||
out.push_str(binary_op_text(*op));
|
|
||||||
out.push(' ');
|
|
||||||
self.write_expr(out, *rhs, indent, prec + 1);
|
|
||||||
}
|
|
||||||
Expr::Default { base, fallback } => {
|
|
||||||
self.write_expr(out, *base, indent, prec);
|
|
||||||
out.push_str(" default ");
|
|
||||||
self.write_expr(out, *fallback, indent, prec + 1);
|
|
||||||
}
|
|
||||||
Expr::CompareConstraint { op, value } => {
|
|
||||||
out.push_str(compare_op_text(*op));
|
|
||||||
out.push(' ');
|
|
||||||
self.write_expr(out, *value, indent, prec);
|
|
||||||
}
|
|
||||||
Expr::RegexConstraint(pattern) => {
|
|
||||||
out.push('/');
|
|
||||||
out.push_str(pattern);
|
|
||||||
out.push('/');
|
|
||||||
}
|
|
||||||
Expr::Wildcard => out.push('_'),
|
|
||||||
}
|
|
||||||
if paren {
|
|
||||||
out.push(')');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_literal(&self, out: &mut String, literal: &Literal) {
|
|
||||||
match literal {
|
|
||||||
Literal::String(value) => write_quoted(out, value),
|
|
||||||
Literal::Int(value) => {
|
|
||||||
let _ = write!(out, "{value}");
|
|
||||||
}
|
|
||||||
Literal::Float(value) => {
|
|
||||||
let text = value.to_string();
|
|
||||||
out.push_str(&text);
|
|
||||||
if !text.contains('.') && !text.contains('e') && !text.contains('E') {
|
|
||||||
out.push_str(".0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Literal::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_object(&self, out: &mut String, fields: &[Field], indent: usize) {
|
|
||||||
if fields.is_empty() {
|
|
||||||
out.push_str("{}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.push('{');
|
|
||||||
out.push('\n');
|
|
||||||
self.write_fields(out, fields, indent + INDENT);
|
|
||||||
write_indent(out, indent);
|
|
||||||
out.push('}');
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_fields(&self, out: &mut String, fields: &[Field], indent: usize) {
|
|
||||||
for field in fields {
|
|
||||||
write_indent(out, indent);
|
|
||||||
self.write_field(out, field, indent);
|
|
||||||
out.push_str(";\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_field(&self, out: &mut String, field: &Field, indent: usize) {
|
|
||||||
for (index, part) in field.path.iter().enumerate() {
|
|
||||||
if index > 0 {
|
|
||||||
out.push('.');
|
|
||||||
}
|
|
||||||
out.push_str(part);
|
|
||||||
}
|
|
||||||
out.push_str(" = ");
|
|
||||||
self.write_expr(out, field.value, indent, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_array(&self, out: &mut String, items: &[ExprId], indent: usize) {
|
|
||||||
if items.is_empty() {
|
|
||||||
out.push_str("[]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if items.iter().all(|item| self.is_inline(*item)) {
|
|
||||||
out.push('[');
|
|
||||||
for (index, item) in items.iter().enumerate() {
|
|
||||||
if index > 0 {
|
|
||||||
out.push_str(", ");
|
|
||||||
}
|
|
||||||
self.write_expr(out, *item, indent, 0);
|
|
||||||
}
|
|
||||||
out.push(']');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.push('[');
|
|
||||||
out.push('\n');
|
|
||||||
for item in items {
|
|
||||||
write_indent(out, indent + INDENT);
|
|
||||||
self.write_expr(out, *item, indent + INDENT, 0);
|
|
||||||
out.push_str(",\n");
|
|
||||||
}
|
|
||||||
write_indent(out, indent);
|
|
||||||
out.push(']');
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_let(&self, out: &mut String, bindings: &[Field], body: ExprId, indent: usize) {
|
|
||||||
out.push_str("let");
|
|
||||||
out.push('\n');
|
|
||||||
self.write_fields(out, bindings, indent + INDENT);
|
|
||||||
write_indent(out, indent);
|
|
||||||
out.push_str("in");
|
|
||||||
out.push('\n');
|
|
||||||
write_indent(out, indent + INDENT);
|
|
||||||
self.write_expr(out, body, indent + INDENT, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_function(&self, out: &mut String, params: &[Param], body: ExprId, indent: usize) {
|
|
||||||
out.push('(');
|
|
||||||
for (index, param) in params.iter().enumerate() {
|
|
||||||
if index > 0 {
|
|
||||||
out.push_str(", ");
|
|
||||||
}
|
|
||||||
self.write_param(out, param, indent);
|
|
||||||
}
|
|
||||||
out.push(')');
|
|
||||||
out.push_str(" =>");
|
|
||||||
if self.is_inline(body) {
|
|
||||||
out.push(' ');
|
|
||||||
self.write_expr(out, body, indent, 0);
|
|
||||||
} else {
|
|
||||||
out.push('\n');
|
|
||||||
write_indent(out, indent + INDENT);
|
|
||||||
self.write_expr(out, body, indent + INDENT, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_param(&self, out: &mut String, param: &Param, indent: usize) {
|
|
||||||
out.push_str(¶m.name);
|
|
||||||
if let Some(constraint) = param.constraint {
|
|
||||||
out.push_str(": ");
|
|
||||||
self.write_expr(out, constraint, indent, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_match(&self, out: &mut String, scrutinee: ExprId, arms: &[MatchArm], indent: usize) {
|
|
||||||
out.push_str("match ");
|
|
||||||
self.write_expr(out, scrutinee, indent, 0);
|
|
||||||
out.push_str(" {");
|
|
||||||
if !arms.is_empty() {
|
|
||||||
out.push('\n');
|
|
||||||
for arm in arms {
|
|
||||||
write_indent(out, indent + INDENT);
|
|
||||||
self.write_expr(out, arm.pattern, indent + INDENT, 0);
|
|
||||||
out.push_str(": ");
|
|
||||||
self.write_expr(out, arm.body, indent + INDENT, 0);
|
|
||||||
out.push_str(";\n");
|
|
||||||
}
|
|
||||||
write_indent(out, indent);
|
|
||||||
}
|
|
||||||
out.push('}');
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_inline(&self, id: ExprId) -> bool {
|
|
||||||
match &self.ast.get(id).expr {
|
|
||||||
Expr::Literal(_)
|
|
||||||
| Expr::Ident(_)
|
|
||||||
| Expr::Import(_)
|
|
||||||
| Expr::RegexConstraint(_)
|
|
||||||
| Expr::Wildcard => true,
|
|
||||||
Expr::Path { base, .. } => self.is_inline(*base),
|
|
||||||
Expr::Call { callee, args } => {
|
|
||||||
self.is_inline(*callee) && args.iter().all(|arg| self.is_inline(*arg))
|
|
||||||
}
|
|
||||||
Expr::Unary { expr, .. } => self.is_inline(*expr),
|
|
||||||
Expr::Binary { lhs, rhs, .. } => self.is_inline(*lhs) && self.is_inline(*rhs),
|
|
||||||
Expr::Default { base, fallback } => self.is_inline(*base) && self.is_inline(*fallback),
|
|
||||||
Expr::CompareConstraint { value, .. } => self.is_inline(*value),
|
|
||||||
Expr::Array(items) => items.iter().all(|item| self.is_inline(*item)),
|
|
||||||
Expr::Object(fields) => fields.is_empty(),
|
|
||||||
Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expr_prec(expr: &Expr) -> u8 {
|
|
||||||
match expr {
|
|
||||||
Expr::Default { .. } => 1,
|
|
||||||
Expr::Binary { op, .. } => binary_prec(*op),
|
|
||||||
Expr::Unary { .. } | Expr::CompareConstraint { .. } => 17,
|
|
||||||
Expr::Path { .. } | Expr::Call { .. } => 19,
|
|
||||||
_ => 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn binary_prec(op: BinaryOp) -> u8 {
|
|
||||||
match op {
|
|
||||||
BinaryOp::Patch => 3,
|
|
||||||
BinaryOp::And => 5,
|
|
||||||
BinaryOp::LogicalOr => 7,
|
|
||||||
BinaryOp::LogicalAnd => 9,
|
|
||||||
BinaryOp::Equal
|
|
||||||
| BinaryOp::NotEqual
|
|
||||||
| BinaryOp::Greater
|
|
||||||
| BinaryOp::GreaterEqual
|
|
||||||
| BinaryOp::Less
|
|
||||||
| BinaryOp::LessEqual => 11,
|
|
||||||
BinaryOp::Concat => 12,
|
|
||||||
BinaryOp::Add | BinaryOp::Sub => 13,
|
|
||||||
BinaryOp::Mul | BinaryOp::Div => 15,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn binary_op_text(op: BinaryOp) -> &'static str {
|
|
||||||
match op {
|
|
||||||
BinaryOp::Add => "+",
|
|
||||||
BinaryOp::Sub => "-",
|
|
||||||
BinaryOp::Mul => "*",
|
|
||||||
BinaryOp::Div => "/",
|
|
||||||
BinaryOp::Concat => "++",
|
|
||||||
BinaryOp::Equal => "==",
|
|
||||||
BinaryOp::NotEqual => "!=",
|
|
||||||
BinaryOp::Greater => ">",
|
|
||||||
BinaryOp::GreaterEqual => ">=",
|
|
||||||
BinaryOp::Less => "<",
|
|
||||||
BinaryOp::LessEqual => "<=",
|
|
||||||
BinaryOp::LogicalAnd => "&&",
|
|
||||||
BinaryOp::LogicalOr => "||",
|
|
||||||
BinaryOp::And => "&",
|
|
||||||
BinaryOp::Patch => "//",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_op_text(op: CompareOp) -> &'static str {
|
|
||||||
match op {
|
|
||||||
CompareOp::Gt => ">",
|
|
||||||
CompareOp::Gte => ">=",
|
|
||||||
CompareOp::Lt => "<",
|
|
||||||
CompareOp::Lte => "<=",
|
|
||||||
CompareOp::Eq => "==",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_indent(out: &mut String, indent: usize) {
|
|
||||||
for _ in 0..indent {
|
|
||||||
out.push(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_quoted(out: &mut String, value: &str) {
|
|
||||||
out.push('"');
|
|
||||||
for ch in value.chars() {
|
|
||||||
match ch {
|
|
||||||
'"' => out.push_str("\\\""),
|
|
||||||
'\\' => out.push_str("\\\\"),
|
|
||||||
'\n' => out.push_str("\\n"),
|
|
||||||
'\r' => out.push_str("\\r"),
|
|
||||||
'\t' => out.push_str("\\t"),
|
|
||||||
ch if ch.is_control() => {
|
|
||||||
let _ = write!(out, "\\u{:04x}", ch as u32);
|
|
||||||
}
|
|
||||||
ch => out.push(ch),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push('"');
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reject_comments(source_id: SourceId, source: &str) -> Result<()> {
|
|
||||||
if let Some(offset) = comment_offset(source) {
|
|
||||||
return Err(Diagnostic::syntax(
|
|
||||||
Span::new(source_id, offset, offset + 1),
|
|
||||||
"formatter does not yet preserve comments",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn comment_offset(source: &str) -> Option<usize> {
|
|
||||||
let bytes = source.as_bytes();
|
|
||||||
let mut index = 0;
|
|
||||||
while index < bytes.len() {
|
|
||||||
match bytes[index] {
|
|
||||||
b'"' => {
|
|
||||||
index += 1;
|
|
||||||
while index < bytes.len() {
|
|
||||||
match bytes[index] {
|
|
||||||
b'\\' => index += 2,
|
|
||||||
b'"' => {
|
|
||||||
index += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => index += 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b'#' => return Some(index),
|
|
||||||
_ => index += 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::parse_source;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn formats_top_level_fields() {
|
|
||||||
let formatted = format_source("port=Int&>=1 default 8080;name=String;").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
formatted,
|
|
||||||
"port = Int & >= 1 default 8080;\nname = String;\n"
|
|
||||||
);
|
|
||||||
parse_source(&formatted).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn formats_nested_objects() {
|
|
||||||
let formatted =
|
|
||||||
format_source("Server={host=String default \"localhost\";port=Int&>0;};").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
formatted,
|
|
||||||
"Server = {\n host = String default \"localhost\";\n port = Int & > 0;\n};\n"
|
|
||||||
);
|
|
||||||
parse_source(&formatted).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn formats_let_function_match_and_array() {
|
|
||||||
let source = "let f=(x:Int)=>match x{_: [1,{a=2;}];};in f(1)";
|
|
||||||
let formatted = format_source(source).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
formatted,
|
|
||||||
"let\n f = (x: Int) =>\n match x {\n _: [\n 1,\n {\n a = 2;\n },\n ];\n };\nin\n f(1)\n"
|
|
||||||
);
|
|
||||||
parse_source(&formatted).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn preserves_needed_parentheses() {
|
|
||||||
let formatted = format_source("value=(1+2)*3;").unwrap();
|
|
||||||
assert_eq!(formatted, "value = (1 + 2) * 3;\n");
|
|
||||||
parse_source(&formatted).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_comments_instead_of_dropping_them() {
|
|
||||||
let error = format_source("# keep me\nvalue = 1;").unwrap_err();
|
|
||||||
assert_eq!(error.message, "formatter does not yet preserve comments");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ pub mod constraints;
|
|||||||
pub mod diagnostic;
|
pub mod diagnostic;
|
||||||
pub mod embedding;
|
pub mod embedding;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod formatter;
|
|
||||||
mod lexer;
|
mod lexer;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
@@ -22,7 +21,6 @@ pub use decodal_derive::Decodal;
|
|||||||
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
||||||
pub use embedding::{HostField, HostValue};
|
pub use embedding::{HostField, HostValue};
|
||||||
pub use eval::{Engine, format_diagnostic_with};
|
pub use eval::{Engine, format_diagnostic_with};
|
||||||
pub use formatter::{format_source, format_source_with_source_id};
|
|
||||||
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
|
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
|
||||||
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
||||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use decodal::{
|
use decodal::{
|
||||||
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
|
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
|
||||||
Span, format_diagnostic_with, format_source,
|
Span, format_diagnostic_with,
|
||||||
};
|
};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
@@ -11,17 +11,6 @@ pub fn evaluate(source: &str) -> String {
|
|||||||
encode_result(evaluate_inner(source))
|
encode_result(evaluate_inner(source))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = formatSource)]
|
|
||||||
pub fn format_source_json(source: &str) -> String {
|
|
||||||
match format_source(source) {
|
|
||||||
Ok(formatted) => format!("{{\"ok\":true,\"source\":{}}}", json_string(&formatted)),
|
|
||||||
Err(error) => format!(
|
|
||||||
"{{\"ok\":false,\"error\":{}}}",
|
|
||||||
json_string(&format_diagnostic_with_root(&error, "playground"))
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = evaluateProject)]
|
#[wasm_bindgen(js_name = evaluateProject)]
|
||||||
pub fn evaluate_project(entry: &str, files_json: &str) -> String {
|
pub fn evaluate_project(entry: &str, files_json: &str) -> String {
|
||||||
encode_result(evaluate_project_inner(entry, files_json))
|
encode_result(evaluate_project_inner(entry, files_json))
|
||||||
@@ -225,13 +214,6 @@ fn json_string(value: &str) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{evaluate_project_inner, normalize_path, resolve_import};
|
use super::{evaluate_project_inner, normalize_path, resolve_import};
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn formats_source_for_web() {
|
|
||||||
let output = super::format_source_json("value={a=1;};");
|
|
||||||
assert!(output.contains("\"ok\":true"));
|
|
||||||
assert!(output.contains("value = {\\n a = 1;\\n};\\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalizes_virtual_paths() {
|
fn normalizes_virtual_paths() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ The public surface is intentionally organized by use case: execute Decodal with
|
|||||||
### Rust crate
|
### Rust crate
|
||||||
|
|
||||||
The `decodal` crate is the primary Rust runtime and embedding API.
|
The `decodal` crate is the primary Rust runtime and embedding API.
|
||||||
It owns parsing, formatting, evaluation, materialization, diagnostics, host-provided values, and schema/decode traits.
|
It owns parsing, evaluation, materialization, diagnostics, host-provided values, and schema/decode traits.
|
||||||
Rust applications should use this crate when they want to load Decodal source, evaluate it, or embed Decodal into a host program.
|
Rust applications should use this crate when they want to load Decodal source, evaluate it, or embed Decodal into a host program.
|
||||||
|
|
||||||
Important paths:
|
Important paths:
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
# Formatting
|
|
||||||
|
|
||||||
Decodal includes an initial source formatter for parsed Decodal code.
|
|
||||||
The formatter is available from the Rust crate, the CLI, the WebAssembly package, and the playground.
|
|
||||||
|
|
||||||
## CLI
|
|
||||||
|
|
||||||
Print formatted source to stdout:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
decodal fmt config.dcdl
|
|
||||||
```
|
|
||||||
|
|
||||||
Format in place:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
decodal fmt --write config.dcdl
|
|
||||||
```
|
|
||||||
|
|
||||||
Read from stdin:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cat config.dcdl | decodal fmt -
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rust
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let formatted = decodal::format_source("Server={port=Int default 8080;};")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## WebAssembly
|
|
||||||
|
|
||||||
```js
|
|
||||||
import init, { formatSource } from 'decodal-wasm';
|
|
||||||
|
|
||||||
await init();
|
|
||||||
const result = JSON.parse(formatSource('Server={port=Int default 8080;};'));
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
console.log(result.source);
|
|
||||||
} else {
|
|
||||||
console.error(result.error);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Current limitation
|
|
||||||
|
|
||||||
The formatter currently rejects source containing line comments rather than silently dropping them.
|
|
||||||
Comment-preserving formatting requires attaching comments to syntax nodes or formatting from a concrete syntax tree, and should be implemented before comment-heavy source is formatted automatically.
|
|
||||||
@@ -7,8 +7,7 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||||||
|
|
||||||
1. [Introduction](./introduction.md)
|
1. [Introduction](./introduction.md)
|
||||||
2. [Components](./components.md)
|
2. [Components](./components.md)
|
||||||
3. [Formatting](./formatting.md)
|
3. [Language Specification](./language/index.md)
|
||||||
4. [Language Specification](./language/index.md)
|
|
||||||
1. [Lexical Structure and Syntax](./language/syntax.md)
|
1. [Lexical Structure and Syntax](./language/syntax.md)
|
||||||
2. [Value](./language/value/index.md)
|
2. [Value](./language/value/index.md)
|
||||||
1. [String](./language/value/string.md)
|
1. [String](./language/value/string.md)
|
||||||
@@ -37,7 +36,7 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||||||
9. [Materialization and Errors](./language/materialization-and-errors.md)
|
9. [Materialization and Errors](./language/materialization-and-errors.md)
|
||||||
10. [Naming Conventions](./language/naming.md)
|
10. [Naming Conventions](./language/naming.md)
|
||||||
11. [Examples](./language/examples.md)
|
11. [Examples](./language/examples.md)
|
||||||
5. [Implementation Design](./design/index.md)
|
4. [Implementation Design](./design/index.md)
|
||||||
1. [Execution Pipeline](./design/execution-pipeline.md)
|
1. [Execution Pipeline](./design/execution-pipeline.md)
|
||||||
2. [Runtime Model](./design/runtime-model.md)
|
2. [Runtime Model](./design/runtime-model.md)
|
||||||
3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md)
|
3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md)
|
||||||
@@ -45,5 +44,5 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||||||
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
|
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
|
||||||
6. [Embedding API](./design/embedding-api.md)
|
6. [Embedding API](./design/embedding-api.md)
|
||||||
7. [Features](./design/features.md)
|
7. [Features](./design/features.md)
|
||||||
6. [Development](./development.md)
|
5. [Development](./development.md)
|
||||||
7. [Open Issues](./open-issues.md)
|
6. [Open Issues](./open-issues.md)
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ deno add jsr:@hare/decodal-wasm
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import init, { evaluateProject, formatSource } from 'decodal-wasm';
|
import init, { evaluateProject } from 'decodal-wasm';
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
const formatted = JSON.parse(formatSource('Server={port=Int default 8080;};'));
|
const result = evaluateProject(JSON.stringify({
|
||||||
console.log(formatted.source);
|
entry: 'main.dcdl',
|
||||||
|
files: {
|
||||||
const result = evaluateProject('main.dcdl', JSON.stringify({
|
|
||||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
'main.dcdl': 'Server = { port = Int default 8080; };',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(JSON.parse(result));
|
console.log(JSON.parse(result));
|
||||||
|
|||||||
-3
@@ -5,15 +5,12 @@ export function evaluate(source: string): string;
|
|||||||
|
|
||||||
export function evaluateProject(entry: string, files_json: string): string;
|
export function evaluateProject(entry: string, files_json: string): string;
|
||||||
|
|
||||||
export function formatSource(source: string): string;
|
|
||||||
|
|
||||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
export interface InitOutput {
|
export interface InitOutput {
|
||||||
readonly memory: WebAssembly.Memory;
|
readonly memory: WebAssembly.Memory;
|
||||||
readonly evaluate: (a: number, b: number) => [number, number];
|
readonly evaluate: (a: number, b: number) => [number, number];
|
||||||
readonly evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
|
readonly evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
|
||||||
readonly formatSource: (a: number, b: number) => [number, number];
|
|
||||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
|||||||
@@ -40,25 +40,6 @@ export function evaluateProject(entry, files_json) {
|
|||||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} source
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function formatSource(source) {
|
|
||||||
let deferred2_0;
|
|
||||||
let deferred2_1;
|
|
||||||
try {
|
|
||||||
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
||||||
const len0 = WASM_VECTOR_LEN;
|
|
||||||
const ret = wasm.formatSource(ptr0, len0);
|
|
||||||
deferred2_0 = ret[0];
|
|
||||||
deferred2_1 = ret[1];
|
|
||||||
return getStringFromWasm0(ret[0], ret[1]);
|
|
||||||
} finally {
|
|
||||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function __wbg_get_imports() {
|
function __wbg_get_imports() {
|
||||||
const import0 = {
|
const import0 = {
|
||||||
__proto__: null,
|
__proto__: null,
|
||||||
|
|||||||
Binary file not shown.
@@ -3,7 +3,6 @@
|
|||||||
export const memory: WebAssembly.Memory;
|
export const memory: WebAssembly.Memory;
|
||||||
export const evaluate: (a: number, b: number) => [number, number];
|
export const evaluate: (a: number, b: number) => [number, number];
|
||||||
export const evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
|
export const evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
|
||||||
export const formatSource: (a: number, b: number) => [number, number];
|
|
||||||
export const __wbindgen_externrefs: WebAssembly.Table;
|
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
|
|||||||
@@ -34,15 +34,15 @@ deno add jsr:@hare/decodal-wasm
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
\`\`\`js
|
\`\`\`js
|
||||||
import init, { evaluateProject, formatSource } from 'decodal-wasm';
|
import init, { evaluateProject } from 'decodal-wasm';
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
const formatted = JSON.parse(formatSource('Server={port=Int default 8080;};'));
|
const result = evaluateProject(JSON.stringify({
|
||||||
console.log(formatted.source);
|
entry: 'main.dcdl',
|
||||||
|
files: {
|
||||||
const result = evaluateProject('main.dcdl', JSON.stringify({
|
|
||||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
'main.dcdl': 'Server = { port = Int default 8080; };',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(JSON.parse(result));
|
console.log(JSON.parse(result));
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export const docs = Object.fromEntries(
|
|||||||
export const nav = [
|
export const nav = [
|
||||||
{ title: 'Introduction', slug: 'introduction' },
|
{ title: 'Introduction', slug: 'introduction' },
|
||||||
{ title: 'Components', slug: 'components' },
|
{ title: 'Components', slug: 'components' },
|
||||||
{ title: 'Formatting', slug: 'formatting' },
|
|
||||||
{
|
{
|
||||||
title: 'Language Specification',
|
title: 'Language Specification',
|
||||||
slug: 'language',
|
slug: 'language',
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
|||||||
</label>
|
</label>
|
||||||
<button id="load-example" type="button">Load</button>
|
<button id="load-example" type="button">Load</button>
|
||||||
<p id="status" class="status">Loading WASM...</p>
|
<p id="status" class="status">Loading WASM...</p>
|
||||||
<button id="format" disabled>Format</button>
|
|
||||||
<button id="run" disabled>Run</button>
|
<button id="run" disabled>Run</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import init, { evaluateProject, formatSource } from 'decodal-wasm';
|
import init, { evaluateProject } from 'decodal-wasm';
|
||||||
import { EditorView, basicSetup } from 'codemirror';
|
import { EditorView, basicSetup } from 'codemirror';
|
||||||
import { keymap } from '@codemirror/view';
|
import { keymap } from '@codemirror/view';
|
||||||
import { decodal } from 'decodal-codemirror';
|
import { decodal } from 'decodal-codemirror';
|
||||||
@@ -10,7 +10,6 @@ const starterProject = playgroundExamples[0];
|
|||||||
const editorHost = document.getElementById('editor');
|
const editorHost = document.getElementById('editor');
|
||||||
const output = document.getElementById('output');
|
const output = document.getElementById('output');
|
||||||
const run = document.getElementById('run');
|
const run = document.getElementById('run');
|
||||||
const formatButton = document.getElementById('format');
|
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
const fileTree = document.getElementById('file-tree');
|
const fileTree = document.getElementById('file-tree');
|
||||||
const activeFile = document.getElementById('active-file');
|
const activeFile = document.getElementById('active-file');
|
||||||
@@ -177,20 +176,6 @@ function execute() {
|
|||||||
output.classList.toggle('error', !result.ok);
|
output.classList.toggle('error', !result.ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatActiveFile() {
|
|
||||||
const result = JSON.parse(formatSource(getEditorText()));
|
|
||||||
if (!result.ok) {
|
|
||||||
output.textContent = result.error;
|
|
||||||
output.classList.add('error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setEditorText(result.source);
|
|
||||||
project.files[project.activePath] = result.source;
|
|
||||||
saveProject();
|
|
||||||
output.textContent = 'Formatted.';
|
|
||||||
output.classList.remove('error');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFileTree() {
|
function renderFileTree() {
|
||||||
const tree = buildTree(Object.keys(project.files).sort());
|
const tree = buildTree(Object.keys(project.files).sort());
|
||||||
fileTree.replaceChildren(renderTreeList(tree.children));
|
fileTree.replaceChildren(renderTreeList(tree.children));
|
||||||
@@ -244,7 +229,6 @@ function compareNodes(a, b) {
|
|||||||
try {
|
try {
|
||||||
await init();
|
await init();
|
||||||
run.disabled = false;
|
run.disabled = false;
|
||||||
formatButton.disabled = false;
|
|
||||||
status.textContent = '';
|
status.textContent = '';
|
||||||
execute();
|
execute();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -252,7 +236,6 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run.addEventListener('click', execute);
|
run.addEventListener('click', execute);
|
||||||
formatButton.addEventListener('click', formatActiveFile);
|
|
||||||
loadExample.addEventListener('click', () => {
|
loadExample.addEventListener('click', () => {
|
||||||
const example = playgroundExamples.find((item) => item.id === exampleSelect.value);
|
const example = playgroundExamples.find((item) => item.id === exampleSelect.value);
|
||||||
if (!example) return;
|
if (!example) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user