@@ -6,7 +6,7 @@ use std::{
|
||||
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
|
||||
format_diagnostic_with, format_source_with_source_id,
|
||||
format_diagnostic_with,
|
||||
};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
@@ -38,17 +38,6 @@ fn run() -> Result<(), String> {
|
||||
println!("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" => {
|
||||
let path = args.next().unwrap_or_else(|| String::from("-"));
|
||||
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> {
|
||||
let root = read_root_source(path).map_err(format_raw_diagnostic)?;
|
||||
let root_name = root.name.clone();
|
||||
@@ -169,11 +143,10 @@ fn print_help() {
|
||||
println!("Decodal - Deferred Constraint Data Language");
|
||||
println!();
|
||||
println!("Usage:");
|
||||
println!(" decodal <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 fmt [--write] <file.dcdl> Format a DCDL file");
|
||||
println!(" decodal - Read DCDL source from stdin");
|
||||
println!(" decodal <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 - Read DCDL source from stdin");
|
||||
}
|
||||
|
||||
fn format_diagnostic_with_root(error: &Diagnostic, root_name: &str) -> String {
|
||||
|
||||
@@ -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 embedding;
|
||||
pub mod eval;
|
||||
pub mod formatter;
|
||||
mod lexer;
|
||||
pub mod module;
|
||||
pub mod parser;
|
||||
@@ -22,7 +21,6 @@ pub use decodal_derive::Decodal;
|
||||
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
||||
pub use embedding::{HostField, HostValue};
|
||||
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 parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
|
||||
Span, format_diagnostic_with, format_source,
|
||||
Span, format_diagnostic_with,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -11,17 +11,6 @@ pub fn evaluate(source: &str) -> String {
|
||||
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)]
|
||||
pub fn evaluate_project(entry: &str, files_json: &str) -> String {
|
||||
encode_result(evaluate_project_inner(entry, files_json))
|
||||
@@ -225,13 +214,6 @@ fn json_string(value: &str) -> String {
|
||||
mod tests {
|
||||
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]
|
||||
fn normalizes_virtual_paths() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -8,7 +8,7 @@ The public surface is intentionally organized by use case: execute Decodal with
|
||||
### Rust crate
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
2. [Components](./components.md)
|
||||
3. [Formatting](./formatting.md)
|
||||
4. [Language Specification](./language/index.md)
|
||||
3. [Language Specification](./language/index.md)
|
||||
1. [Lexical Structure and Syntax](./language/syntax.md)
|
||||
2. [Value](./language/value/index.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)
|
||||
10. [Naming Conventions](./language/naming.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)
|
||||
2. [Runtime Model](./design/runtime-model.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)
|
||||
6. [Embedding API](./design/embedding-api.md)
|
||||
7. [Features](./design/features.md)
|
||||
6. [Development](./development.md)
|
||||
7. [Open Issues](./open-issues.md)
|
||||
5. [Development](./development.md)
|
||||
6. [Open Issues](./open-issues.md)
|
||||
|
||||
@@ -22,15 +22,15 @@ deno add jsr:@hare/decodal-wasm
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import init, { evaluateProject, formatSource } from 'decodal-wasm';
|
||||
import init, { evaluateProject } from 'decodal-wasm';
|
||||
|
||||
await init();
|
||||
|
||||
const formatted = JSON.parse(formatSource('Server={port=Int default 8080;};'));
|
||||
console.log(formatted.source);
|
||||
|
||||
const result = evaluateProject('main.dcdl', JSON.stringify({
|
||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
||||
const result = evaluateProject(JSON.stringify({
|
||||
entry: 'main.dcdl',
|
||||
files: {
|
||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
||||
},
|
||||
}));
|
||||
|
||||
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 formatSource(source: string): string;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly evaluate: (a: number, b: 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_malloc: (a: number, b: 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,6 @@
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const evaluate: (a: number, b: 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_malloc: (a: number, b: 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
|
||||
|
||||
\`\`\`js
|
||||
import init, { evaluateProject, formatSource } from 'decodal-wasm';
|
||||
import init, { evaluateProject } from 'decodal-wasm';
|
||||
|
||||
await init();
|
||||
|
||||
const formatted = JSON.parse(formatSource('Server={port=Int default 8080;};'));
|
||||
console.log(formatted.source);
|
||||
|
||||
const result = evaluateProject('main.dcdl', JSON.stringify({
|
||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
||||
const result = evaluateProject(JSON.stringify({
|
||||
entry: 'main.dcdl',
|
||||
files: {
|
||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
||||
},
|
||||
}));
|
||||
|
||||
console.log(JSON.parse(result));
|
||||
|
||||
@@ -20,7 +20,6 @@ export const docs = Object.fromEntries(
|
||||
export const nav = [
|
||||
{ title: 'Introduction', slug: 'introduction' },
|
||||
{ title: 'Components', slug: 'components' },
|
||||
{ title: 'Formatting', slug: 'formatting' },
|
||||
{
|
||||
title: 'Language Specification',
|
||||
slug: 'language',
|
||||
|
||||
@@ -15,7 +15,6 @@ import ManualLayout from '../layouts/ManualLayout.astro';
|
||||
</label>
|
||||
<button id="load-example" type="button">Load</button>
|
||||
<p id="status" class="status">Loading WASM...</p>
|
||||
<button id="format" disabled>Format</button>
|
||||
<button id="run" disabled>Run</button>
|
||||
</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 { keymap } from '@codemirror/view';
|
||||
import { decodal } from 'decodal-codemirror';
|
||||
@@ -10,7 +10,6 @@ const starterProject = playgroundExamples[0];
|
||||
const editorHost = document.getElementById('editor');
|
||||
const output = document.getElementById('output');
|
||||
const run = document.getElementById('run');
|
||||
const formatButton = document.getElementById('format');
|
||||
const status = document.getElementById('status');
|
||||
const fileTree = document.getElementById('file-tree');
|
||||
const activeFile = document.getElementById('active-file');
|
||||
@@ -177,20 +176,6 @@ function execute() {
|
||||
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() {
|
||||
const tree = buildTree(Object.keys(project.files).sort());
|
||||
fileTree.replaceChildren(renderTreeList(tree.children));
|
||||
@@ -244,7 +229,6 @@ function compareNodes(a, b) {
|
||||
try {
|
||||
await init();
|
||||
run.disabled = false;
|
||||
formatButton.disabled = false;
|
||||
status.textContent = '';
|
||||
execute();
|
||||
} catch (error) {
|
||||
@@ -252,7 +236,6 @@ try {
|
||||
}
|
||||
|
||||
run.addEventListener('click', execute);
|
||||
formatButton.addEventListener('click', formatActiveFile);
|
||||
loadExample.addEventListener('click', () => {
|
||||
const example = playgroundExamples.find((item) => item.id === exampleSelect.value);
|
||||
if (!example) return;
|
||||
|
||||
Reference in New Issue
Block a user