Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2017c4f
Initial plan
Copilot Dec 8, 2025
4829014
Add BNF support package with ANTLR grammars and bidirectional converters
Copilot Dec 8, 2025
ed2e54e
Add examples for BNF conversion
Copilot Dec 8, 2025
b44b667
Improve operator extraction logic and add grammar design notes
Copilot Dec 8, 2025
a0078b2
Add comprehensive implementation summary documentation
Copilot Dec 8, 2025
1d53c6a
Restructure BNF package: move configs to root, rename to atsds-bnf/ap…
Copilot Dec 8, 2025
a7267c9
Remove examples, tests, and TypeScript; simplify to basic JavaScript …
Copilot Dec 8, 2025
d857bd4
Address feedback: rename src to atsds_bnf, remove cli.py, use setupto…
Copilot Dec 8, 2025
944b137
Add package-lock.json.
hzhangxyz Dec 8, 2025
4d9e30a
Add uv.lock.
hzhangxyz Dec 8, 2025
3deffca
Add .gitignore.
hzhangxyz Dec 8, 2025
8c73c76
Run pre-commit.
hzhangxyz Dec 8, 2025
2bc8ab8
Update README.md.
hzhangxyz Dec 8, 2025
0648afc
Update bnf binding for js.
hzhangxyz Dec 8, 2025
65b4e79
Refactor BNF package: 1:1 match JS/Python, add rollup, clean dependen…
Copilot Dec 8, 2025
3b671b3
Remove useless variables.
hzhangxyz Dec 8, 2025
3890577
Fix python binding.
hzhangxyz Dec 8, 2025
eb8eb52
Update uv.lock.
hzhangxyz Dec 8, 2025
b180301
Update package lock.
hzhangxyz Dec 8, 2025
80e2b46
Update pyproject.toml.
hzhangxyz Dec 8, 2025
29c49f4
Update package.json.
hzhangxyz Dec 8, 2025
d145b09
Move *.g4.
hzhangxyz Dec 8, 2025
6de97da
Update package.json.
hzhangxyz Dec 8, 2025
733e393
Update package.json.
hzhangxyz Dec 8, 2025
dea927d
Update *.g4.
hzhangxyz Dec 8, 2025
9d00ffa
Update setup.py.
hzhangxyz Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions bnf/Ds.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
grammar Ds;

rule_pool
: NEWLINE* (rule (NEWLINE+ rule)*)? NEWLINE* EOF
;

rule
: (term NEWLINE+)* RULE NEWLINE term
;

term
: SYMBOL # symbol
| '(subscript' term* ')' # subscript
| '(function' term* ')' # function
| '(unary' SYMBOL term ')' # unary
| '(binary' SYMBOL term term ')' # binary
;

RULE
: '--' '-'*
;

WHITESPACE
: [ \t]+ -> skip
;

COMMENT
: '//' ~[\r\n]* -> skip
;

NEWLINE
: [\r\n]
;

SYMBOL
: ~[ \t\r\n,()[\]]+

Copilot AI Dec 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The SYMBOL token definition in both grammars includes [ and ] in the exclusion set, but the Ds grammar doesn't use these characters. While this doesn't cause issues, it's inconsistent with the actual syntax being parsed. The Dsp grammar needs this exclusion because it uses brackets for subscripts, but Ds uses s-expressions only.

Suggested change
: ~[ \t\r\n,()[\]]+
: ~[ \t\r\n,()]+

Copilot uses AI. Check for mistakes.
;
48 changes: 48 additions & 0 deletions bnf/Dsp.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
grammar Dsp;

rule_pool
: NEWLINE* (rule (NEWLINE+ rule)*)? NEWLINE* EOF
;

rule
: term
| (term (',' term)*)? '->' term

Copilot AI Dec 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule grammar allows (term (',' term)*)? '->' term which permits zero terms before the arrow (e.g., -> conclusion). This creates an ambiguous parse: is -> conclusion a rule with zero premises, or is it a binary expression? This could lead to unexpected parsing behavior.

Consider making at least one premise mandatory when using the arrow syntax, or add explicit disambiguation logic in the parser.

Suggested change
| (term (',' term)*)? '->' term
| term (',' term)* '->' term

Copilot uses AI. Check for mistakes.

Copilot AI Dec 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dsp grammar rule on line 9 allows an empty premise list with the optional (term (',' term)*)? before '->'. This means -> conclusion is valid syntax. However, the ParseVisitor doesn't handle this case - when result is empty after visiting terms, result.pop() on line 22 will fail with an IndexError. The visitor should check for empty premises.

Copilot uses AI. Check for mistakes.
;

term
: SYMBOL # symbol
| '(' term ')' # parentheses
| term '::' term # binary
| term '.' term # binary
| term '[' term (',' term)* ']' # subscript
| term '(' (term (',' term)*)? ')' # function
| <assoc=right> ('~' | '!' | '-' | '+' | '&' | '*') term # unary
| term '.*' term # binary
| term ('*' | '/' | '%') term # binary
| term ('+' | '-') term # binary
| term ('<<' | '>>') term # binary
| term ('<' | '>' | '<=' | '>=') term # binary
| term ('==' | '!=') term # binary
| term '&' term # binary
| term '^' term # binary
| term '|' term # binary
| term '&&' term # binary
| term '||' term # binary
| term '=' term # binary
;

WHITESPACE
: [ \t]+ -> skip
;

COMMENT
: '//' ~[\r\n]* -> skip
;

NEWLINE
: [\r\n]
;

SYMBOL
: ~[ \t\r\n,()[\]]+
;
6 changes: 6 additions & 0 deletions bnf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# BNF Support Package for DS

This package provides bidirectional conversion between two syntax formats for the DS deductive system:

- **Ds**: The lisp-like syntax currently used in DS
- **Dsp**: A traditional readable syntax with infix operators
2 changes: 2 additions & 0 deletions bnf/apyds_bnf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!__init__.py
88 changes: 88 additions & 0 deletions bnf/apyds_bnf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
__all__ = ["parse", "unparse"]

from antlr4 import InputStream, CommonTokenStream
from .DspLexer import DspLexer
from .DspParser import DspParser
from .DspVisitor import DspVisitor
from .DsLexer import DsLexer
from .DsParser import DsParser
from .DsVisitor import DsVisitor


class ParseVisitor(DspVisitor):
def visitRule_pool(self, ctx):
return "\n\n".join(self.visit(r) for r in ctx.rule_())

def visitRule(self, ctx):
result = [self.visit(t) for t in ctx.term()]
if len(result) == 1:
return f"----\n{result[0]}"
else:
conclusion = result.pop()
length = max(len(premise) for premise in result)
result.append("-" * max(length, 4))
result.append(conclusion)
return "\n".join(result)

def visitSymbol(self, ctx):
return ctx.SYMBOL().getText()

def visitParentheses(self, ctx):
return self.visit(ctx.term())

def visitSubscript(self, ctx):
return f"(subscript {' '.join(self.visit(t) for t in ctx.term())})"

def visitFunction(self, ctx):
return f"(function {' '.join(self.visit(t) for t in ctx.term())})"

def visitUnary(self, ctx):
return f"(unary {ctx.getChild(0).getText()} {self.visit(ctx.term())})"

def visitBinary(self, ctx):
return f"(binary {ctx.getChild(1).getText()} {self.visit(ctx.term(0))} {self.visit(ctx.term(1))})"


class UnparseVisitor(DsVisitor):
def visitRule_pool(self, ctx):
return "\n".join(self.visit(r) for r in ctx.rule_())

def visitRule(self, ctx):
result = [self.visit(t) for t in ctx.term()]
conclusion = result.pop()
return ", ".join(result) + " -> " + conclusion
Comment on lines +52 to +53

Copilot AI Dec 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same asymmetry issue exists in the Python implementation. When there's only one term in a rule (lines 18-19), it's formatted as "----\n{result[0]}", but in UnparseVisitor.visitRule (lines 50-53), result.pop() is always called without checking if there are enough elements. This will fail for rules with only a conclusion.

Suggested change
conclusion = result.pop()
return ", ".join(result) + " -> " + conclusion
if len(result) == 1:
return result[0]
else:
conclusion = result.pop()
return ", ".join(result) + " -> " + conclusion

Copilot uses AI. Check for mistakes.

def visitSymbol(self, ctx):
return ctx.SYMBOL().getText()

def visitSubscript(self, ctx):
return f"{self.visit(ctx.term(0))}[{', '.join(self.visit(t) for t in ctx.term()[1:])}]"

def visitFunction(self, ctx):
return f"{self.visit(ctx.term(0))}({', '.join(self.visit(t) for t in ctx.term()[1:])})"

def visitUnary(self, ctx):
return f"({ctx.getChild(0).getText()} {self.visit(ctx.term())})"

def visitBinary(self, ctx):
return f"({self.visit(ctx.term(0))} {ctx.getChild(1).getText()} {self.visit(ctx.term(1))})"


def parse(input):

Copilot AI Dec 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The parameter name is input which shadows the built-in Python function input(). While this works, it's considered poor practice. Consider renaming to input_str, text, or source to avoid shadowing built-ins.

Copilot uses AI. Check for mistakes.
chars = InputStream(input)
lexer = DspLexer(chars)
tokens = CommonTokenStream(lexer)
parser = DspParser(tokens)
tree = parser.rule_pool()
visitor = ParseVisitor()
return visitor.visit(tree)


def unparse(input):

Copilot AI Dec 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The parameter name is input which shadows the built-in Python function input(). While this works, it's considered poor practice. Consider renaming to input_str, text, or source to avoid shadowing built-ins.

Copilot uses AI. Check for mistakes.
chars = InputStream(input)
lexer = DsLexer(chars)
tokens = CommonTokenStream(lexer)
parser = DsParser(tokens)
tree = parser.rule_pool()
visitor = UnparseVisitor()
return visitor.visit(tree)
2 changes: 2 additions & 0 deletions bnf/atsds_bnf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!index.js

Copilot AI Dec 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .gitignore pattern * followed by !index.js will ignore all files except index.js. However, the actual source file is index.mjs (as seen in the diff), not index.js. This means the .gitignore won't preserve the intended file and will instead preserve a non-existent file.

Suggested change
!index.js
!index.mjs

Copilot uses AI. Check for mistakes.
122 changes: 122 additions & 0 deletions bnf/atsds_bnf/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { InputStream, CommonTokenStream } from "antlr4";
import DspLexer from "./DspLexer.js";
import DspParser from "./DspParser.js";
import DspVisitor from "./DspVisitor.js";
import DsLexer from "./DsLexer.js";
import DsParser from "./DsParser.js";
import DsVisitor from "./DsVisitor.js";

class ParseVisitor extends DspVisitor {
visitRule_pool(ctx) {
return ctx
.rule_()
.map((r) => this.visit(r))
.join("\n\n");
}

visitRule(ctx) {
const result = ctx.term().map((t) => this.visit(t));
if (result.length === 1) {
return `----\n${result[0]}`;
} else {
const conclusion = result.pop();
const length = Math.max(...result.map((premise) => premise.length));
result.push("-".repeat(Math.max(length, 4)));
result.push(conclusion);
return result.join("\n");
}
}

visitSymbol(ctx) {
return ctx.SYMBOL().getText();
}

visitParentheses(ctx) {
return this.visit(ctx.term());
}

visitSubscript(ctx) {
return `(subscript ${ctx
.term()
.map((t) => this.visit(t))
.join(" ")})`;
}

visitFunction(ctx) {
return `(function ${ctx
.term()
.map((t) => this.visit(t))
.join(" ")})`;
}

visitUnary(ctx) {
return `(unary ${ctx.getChild(0).getText()} ${this.visit(ctx.term())})`;
}

visitBinary(ctx) {
return `(binary ${ctx.getChild(1).getText()} ${this.visit(ctx.term(0))} ${this.visit(ctx.term(1))})`;
}
}

class UnparseVisitor extends DsVisitor {
visitRule_pool(ctx) {
return ctx
.rule_()
.map((r) => this.visit(r))
.join("\n");
}

visitRule(ctx) {
const result = ctx.term().map((t) => this.visit(t));
const conclusion = result.pop();
return result.join(", ") + " -> " + conclusion;
Comment on lines +71 to +72

Copilot AI Dec 9, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the JavaScript implementation, when there's only one premise in a rule (line 19-20), the result is formatted as "----\n{result[0]}". However, in the UnparseVisitor.visitRule (line 69-72), when converting back, result.pop() is always called, which assumes there are at least 2 elements. This means a rule with only a conclusion (no premises) would fail. The parse and unparse operations are not symmetric for edge cases.

Suggested change
const conclusion = result.pop();
return result.join(", ") + " -> " + conclusion;
if (result.length === 1) {
// Only a conclusion, no premises
return "----\n" + result[0];
} else {
const conclusion = result.pop();
return result.join(", ") + " -> " + conclusion;
}

Copilot uses AI. Check for mistakes.
}

visitSymbol(ctx) {
return ctx.SYMBOL().getText();
}

visitSubscript(ctx) {
return `${this.visit(ctx.term(0))}[${ctx
.term()
.slice(1)
.map((t) => this.visit(t))
.join(", ")}]`;
}

visitFunction(ctx) {
return `${this.visit(ctx.term(0))}(${ctx
.term()
.slice(1)
.map((t) => this.visit(t))
.join(", ")})`;
}

visitUnary(ctx) {
return `(${ctx.getChild(0).getText()} ${this.visit(ctx.term())})`;
}

visitBinary(ctx) {
return `(${this.visit(ctx.term(0))} ${ctx.getChild(1).getText()} ${this.visit(ctx.term(1))})`;
}
}

export function parse(input) {
const chars = new InputStream(input);
const lexer = new DspLexer(chars);
const tokens = new CommonTokenStream(lexer);
const parser = new DspParser(tokens);
const tree = parser.rule_pool();
const visitor = new ParseVisitor();
return visitor.visit(tree);
}

export function unparse(input) {
const chars = new InputStream(input);
const lexer = new DsLexer(chars);
const tokens = new CommonTokenStream(lexer);
const parser = new DsParser(tokens);
const tree = parser.rule_pool();
const visitor = new UnparseVisitor();
return visitor.visit(tree);
}
Loading