i'm redoing all the parsing lol
parent
d152c4092a
commit
cd51f4cce1
@ -1,3 +1,98 @@
|
|||||||
# clyde
|
# clyde
|
||||||
|
|
||||||
A command-line shell. Initial development is for Windows because Windows has the worst starting point when it comes to CLI shells.
|
A command-line shell. Initial development is for Windows because Windows has
|
||||||
|
the worst starting point when it comes to CLI shells. Assuming nothing here
|
||||||
|
works and it doesn't even compile.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
The needs of this project reflect my professional experience working on
|
||||||
|
multiplayer video games. Prior to working in the games industry, I worked in
|
||||||
|
the web industry, primarily writing server software. The working practices of
|
||||||
|
programmers in these industries differ substantially. The large differences in
|
||||||
|
these working practices create a substantial and difficult social divide
|
||||||
|
within studios that make multiplayer games. These social differences make an
|
||||||
|
already-difficult category of software even more difficult to develop. The
|
||||||
|
goal of this project is to reduce the tooling divide that exists between
|
||||||
|
client and server developers at multiplayer game studios, with the expectation
|
||||||
|
that shrinking this tooling gap can reduce the social divide that exists
|
||||||
|
between game developers and server/infrastructure developers at game studios.
|
||||||
|
|
||||||
|
### Windows is a hard requirement
|
||||||
|
|
||||||
|
The first gap that appears within the greater software developer landscape is
|
||||||
|
the question of using Windows at all. Let us dispense with this question
|
||||||
|
immediately: Windows is a necesary tool within the game development industry.
|
||||||
|
Supporting Windows is a hard requirement of this project.
|
||||||
|
|
||||||
|
Many necessary tools, such as tools provided by game console manufacturers and
|
||||||
|
tools that integrate with the dominant game engines (Unity and Unreal) only
|
||||||
|
support Windows. If your reaction to this is "don't use those tools" or "make
|
||||||
|
your game in Godot", please just stop. This is a very common reaction from
|
||||||
|
professional programmers who have never worked in gamedev, who don't
|
||||||
|
understand the constraints that most game developers face. Unfortunately a
|
||||||
|
large portion of programming discussion happens on Hacker News, Reddit, and
|
||||||
|
Lobsters, each of which uses a democratic structure.
|
||||||
|
|
||||||
|
I will not attempt to convince you that Windows is broadly
|
||||||
|
unavoidable in professional game development and that Windows is a necessary
|
||||||
|
tool for game development studios, even studios that employ engineers for whom
|
||||||
|
Windows is not necessary for their individual role. If you want to advocate
|
||||||
|
for Linux superiority, please go bother Nintendo, Sony, Microsoft, etc, not
|
||||||
|
individual game developers. If you really can't get over this and feel the
|
||||||
|
need to yell at me, please walk a few hundred yards into a dense forest and
|
||||||
|
scream your complaints into the wilderness to feel better.
|
||||||
|
|
||||||
|
The command-line environments built into Windows are incredibly primitive.
|
||||||
|
These environments are so primitive that millions of professional programmers
|
||||||
|
who work primarily on Windows are led to believe that command-line
|
||||||
|
environments themselves are *inherently* primitive.
|
||||||
|
|
||||||
|
. The limitations of built-in
|
||||||
|
command-line environments on Windows is so severe that there are entire
|
||||||
|
categories of professional computer programmers who believe that they are "not
|
||||||
|
terminal people", often without realizing that the terminal on Linux and MacOS
|
||||||
|
is a vastly different experience and different tooling ecosystem than it is on
|
||||||
|
Windows. Many professional Windows developers believe that CLI environments
|
||||||
|
are inherently primitive, because their only exposure to CLI environments is
|
||||||
|
through the built-in Windows shells.
|
||||||
|
|
||||||
|
Windows ships with two built-in
|
||||||
|
shells. The first shell is the Command Prompt, which we'll refer to as
|
||||||
|
[cmd.exe](https://en.wikipedia.org/wiki/Cmd.exe) as it is colloquially known,
|
||||||
|
even when running inside of
|
||||||
|
[terminal.exe](https://en.wikipedia.org/wiki/Windows_Terminal). The second
|
||||||
|
shell is [PowerShell](https://en.wikipedia.org/wiki/PowerShell).
|
||||||
|
|
||||||
|
### Insufficiency of cmd.exe
|
||||||
|
|
||||||
|
The insufficiency of cmd.exe is widely understood to people with experience
|
||||||
|
with Unix-like operating systems such as Linux and MacOS, which typically
|
||||||
|
include either [bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) or
|
||||||
|
[zsh](https://en.wikipedia.org/wiki/Z_shell). This insufficiency is severe
|
||||||
|
enough that it drives people off of Windows entirely.
|
||||||
|
|
||||||
|
In some industries, such as games, Windows is a required development
|
||||||
|
environment, because many tools are available only on Windows. Free software
|
||||||
|
purists who want to insist that you can make video games on Linux, please feel
|
||||||
|
free to leave now, this project is not for you.
|
||||||
|
|
||||||
|
## Terimology
|
||||||
|
|
||||||
|
- **executable**: a shell built-in or an executable file (i.e., an .exe file)
|
||||||
|
|
||||||
|
## examples
|
||||||
|
|
||||||
|
$> a
|
||||||
|
|
||||||
|
Find an executable named `a` somewhere on `PATH` and execute it in the
|
||||||
|
foreground.
|
||||||
|
|
||||||
|
$> ./a
|
||||||
|
|
||||||
|
Use an executable named `a` within the current directory and execute it in the
|
||||||
|
foreground. `a` must be a file in the current directory. If `a` would refer to
|
||||||
|
both a shell built-in and a file in the current directory, use the file in the
|
||||||
|
current directory.
|
||||||
|
|
||||||
|
$>
|
||||||
|
@ -0,0 +1,278 @@
|
|||||||
|
use crate::error::ParseError;
|
||||||
|
use crate::lex::{Lexer, Token};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::VecDeque,
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum Value {
|
||||||
|
/// The start symbol of our parse tree. Each parse tree is rooted in a node whose value is the
|
||||||
|
/// start symbol. This is the only node in the tree that should utilize the start symbol.
|
||||||
|
Start,
|
||||||
|
|
||||||
|
Statement,
|
||||||
|
|
||||||
|
Terminal(Token),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
fn is_terminal(&self) -> bool {
|
||||||
|
matches!(self, Value::Terminal(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A node in a parse tree.
|
||||||
|
pub struct Node {
|
||||||
|
/// A node may or may not have a parent node. If a node does not have a parent node, that node
|
||||||
|
/// is the root node of a tree.
|
||||||
|
parent: Option<Weak<Node>>,
|
||||||
|
|
||||||
|
/// The value of the element at this node
|
||||||
|
value: Value,
|
||||||
|
|
||||||
|
/// A node may or may not have children. Since an empty vector is a valid vector, a node
|
||||||
|
/// without children is represented as having an empty children vector. A node having an empty
|
||||||
|
/// list of children is a leaf node in a tree.
|
||||||
|
children: RefCell<Vec<Rc<Node>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
fn new() -> Cursor {
|
||||||
|
let root = Node {
|
||||||
|
parent: None,
|
||||||
|
value: Value::Start,
|
||||||
|
children: RefCell::new(Vec::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let root = Rc::new(root);
|
||||||
|
Cursor {
|
||||||
|
target: Rc::clone(&root),
|
||||||
|
root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cursor values expose access to a parse tree.
|
||||||
|
struct Cursor {
|
||||||
|
target: Rc<Node>,
|
||||||
|
root: Rc<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cursor {
|
||||||
|
/// Climbs one level up a parse tree. The cursor is re-pointed from its current target node to
|
||||||
|
/// the parent of its current target node. This method fails if the cursor is already at the
|
||||||
|
/// root node of the parse tree.
|
||||||
|
fn up(&mut self) -> Result<(), ParseError> {
|
||||||
|
match &self.target.parent {
|
||||||
|
None => Err(ParseError::AtRootAlready),
|
||||||
|
Some(parent) => match parent.upgrade() {
|
||||||
|
Some(parent) => {
|
||||||
|
self.target = parent;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
None => Err(ParseError::ParentIsGone),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a value to the children of the current target node, then descends to select that
|
||||||
|
/// child.
|
||||||
|
fn push(&mut self, v: Value) -> Result<(), ParseError> {
|
||||||
|
if self.target.value.is_terminal() {
|
||||||
|
return Err(ParseError::PushOntoTerminal);
|
||||||
|
}
|
||||||
|
let node = Node {
|
||||||
|
parent: Some(Rc::downgrade(&self.target)),
|
||||||
|
value: v,
|
||||||
|
children: RefCell::new(Vec::new()),
|
||||||
|
};
|
||||||
|
let node = Rc::new(node);
|
||||||
|
self.target
|
||||||
|
.children
|
||||||
|
.try_borrow_mut()?
|
||||||
|
.push(Rc::clone(&node));
|
||||||
|
self.target = node;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_root(&self) -> bool {
|
||||||
|
self.target.parent.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_root(self) -> Rc<Node> {
|
||||||
|
Rc::clone(&self.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(&self) -> &Value {
|
||||||
|
&self.target.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Parser<'text> {
|
||||||
|
source: Lexer<'text>,
|
||||||
|
cursor: Cursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'text> Parser<'text> {
|
||||||
|
pub fn new(source: Lexer<'text>) -> Self {
|
||||||
|
Self {
|
||||||
|
source,
|
||||||
|
cursor: Node::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(mut self) -> Result<Rc<Node>, ParseError> {
|
||||||
|
while self.step()? {}
|
||||||
|
Ok(self.cursor.into_root())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(&mut self) -> Result<bool, ParseError> {
|
||||||
|
match self.cursor.value() {
|
||||||
|
Value::Start => self.step_start(),
|
||||||
|
Value::Statement => self.step_statement(),
|
||||||
|
Value::Terminal(_) => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_start(&mut self) -> Result<bool, ParseError> {
|
||||||
|
assert!(matches!(self.cursor.value(), Value::Start));
|
||||||
|
match self.source.peek()? {
|
||||||
|
Some(Token::String(_)) => {
|
||||||
|
self.cursor.push(Value::Statement)?;
|
||||||
|
let token = self.source.next().unwrap()?;
|
||||||
|
self.cursor.push(Value::Terminal(token))?;
|
||||||
|
self.cursor.up()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Some(Token::Glob(_)) => {
|
||||||
|
let token = self.source.next().unwrap()?;
|
||||||
|
Err(ParseError::UnexpectedToken(token))
|
||||||
|
}
|
||||||
|
None => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_statement(&mut self) -> Result<bool, ParseError> {
|
||||||
|
assert!(matches!(self.cursor.value(), Value::Statement));
|
||||||
|
match self.source.peek()? {
|
||||||
|
Some(Token::String(_) | Token::Glob(_)) => {
|
||||||
|
let token = self.source.next().unwrap()?;
|
||||||
|
self.cursor.push(Value::Terminal(token))?;
|
||||||
|
self.cursor.up()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
None => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(source: &str) -> Result<Rc<Node>, ParseError> {
|
||||||
|
let tokens = Lexer::new(source);
|
||||||
|
let parser = Parser::new(tokens);
|
||||||
|
parser.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::lex::lex;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root() {
|
||||||
|
let mut cursor = Node::new();
|
||||||
|
assert!(cursor.up().is_err());
|
||||||
|
assert!(cursor.target.value == Value::Start);
|
||||||
|
assert!(cursor.is_root());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_val() {
|
||||||
|
let mut cursor = Node::new();
|
||||||
|
|
||||||
|
let mut tokens = lex(" ls ").unwrap();
|
||||||
|
let ls = tokens.pop().unwrap();
|
||||||
|
assert!(cursor.push(Value::Statement).is_ok());
|
||||||
|
assert!(cursor.push(Value::Terminal(ls.clone())).is_ok());
|
||||||
|
assert!(cursor.push(Value::Terminal(ls.clone())).is_err());
|
||||||
|
assert!(cursor.target.value == Value::Terminal(ls));
|
||||||
|
assert!(!cursor.is_root());
|
||||||
|
assert!(cursor.up().is_ok());
|
||||||
|
assert!(cursor.up().is_ok());
|
||||||
|
assert!(cursor.is_root());
|
||||||
|
assert!(cursor.up().is_err());
|
||||||
|
assert!(cursor.value() == &Value::Start);
|
||||||
|
let root = cursor.into_root();
|
||||||
|
assert!(root.value == Value::Start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() -> Result<(), ParseError> {
|
||||||
|
parse("ls")?;
|
||||||
|
// parse("*")?;
|
||||||
|
// parse("x* ls")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
> ls
|
||||||
|
|
||||||
|
start
|
||||||
|
statement
|
||||||
|
ls
|
||||||
|
|
||||||
|
> ls ;
|
||||||
|
|
||||||
|
start
|
||||||
|
statement
|
||||||
|
ls
|
||||||
|
;
|
||||||
|
|
||||||
|
> ls ; ls
|
||||||
|
|
||||||
|
start
|
||||||
|
statement
|
||||||
|
ls
|
||||||
|
;
|
||||||
|
statement
|
||||||
|
ls
|
||||||
|
|
||||||
|
> ls one two three
|
||||||
|
|
||||||
|
start
|
||||||
|
statement
|
||||||
|
ls
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
|
||||||
|
> ls > files.txt ; echo files.txt
|
||||||
|
|
||||||
|
start
|
||||||
|
statement
|
||||||
|
ls
|
||||||
|
>
|
||||||
|
files.txt
|
||||||
|
;
|
||||||
|
statement
|
||||||
|
echo
|
||||||
|
files.txt
|
||||||
|
|
||||||
|
> if exists ~/.vimrc : echo you have a vimrc
|
||||||
|
|
||||||
|
> if $x == 3: echo hi
|
||||||
|
|
||||||
|
start
|
||||||
|
if
|
||||||
|
expression
|
||||||
|
$x
|
||||||
|
==
|
||||||
|
3
|
||||||
|
:
|
||||||
|
statement
|
||||||
|
echo
|
||||||
|
hi
|
||||||
|
*/
|
Loading…
Reference in New Issue