WIP scanning, analysis and AST todo

This commit is contained in:
Nate Anderson 2025-07-17 17:47:08 -06:00
parent dc6b360583
commit 38eab409c6
9 changed files with 536 additions and 38 deletions

View File

@ -1,4 +1,61 @@
[Duct Tape] DuctTape:
d:The tape that does it all. str="Duct Tape"
status=[.Full, .Empty]
location=Warehouse
[Flash Light] FlashLight:
str="Flash Light"
status=[.Bright, .Dead, .Broken]
_onExamine:
if $self.status = .Bright:
A handy tool for seeing your way out.
else if $self.status = .Dead:
A handy tool for seeing your way out, unfortunately its out of batteries.
else:
A handy tool for seeing your way out, unfortunately its busted.
_onUse:
if $context.args.contains(Batteries) and $self.status = .Dead:
$self.status = .Bright
$player.inventory.remove(Batteries)
You screw the back on tight, and the light comes on!
_onFix:
if $context.args.contains(DuctTape) and $self.status = .Broken:
$self.status = .Dead
$DuctTape.status = .Empty
Nothing duct tape cant fix! The flashlight just needs some power.
Batteries:
_onExamine:
Energized little cylinders, good for something...
# Hammer:
# [Hammer]
# desc= Good for breaking things
# status=Solid
# rel=Flash Light=>Broken
# [Key1]
# name= Key
# desc= A mysterious key
# [Key2]
# name= Key
# desc= A different key, not to be confused with the other
# [Locked Chest]
# decs= A large chest with two key holes
# status=Locked,Closed,Opened
# *Open=
# &Locked::Its locked
# &Opened::Its already open
# &:s.Opened:The large chest swings open.
# *Close=
# &Locked::[Its locked, and therefore closed. You arent strong enough to break it with your hands.<Hammer.s=&Solid>You cant break it open, but maybe with something tough enough...]
# &Closed::Its already closed!!
# &Opened:s.Closed:The chest closes with a clunk.
# *Unlock=
# &

161
cyo_spec.md Normal file
View File

@ -0,0 +1,161 @@
# Mindset
In the world of text adventures, at any given moment, a player:
- Resides in a specific location
- Can issue any arbitrary command
- Said command should never cause an error
- Posseses an inventory with any number of objects
-
All entities in a text adventure world are typically broken into two categories: scenes and objects.
But, I believe there is good reason to abstract all entities into just objects.
Then, objects are duck-typed into specific things by way of their available methods.
Methods include but are not limited to:
- `_onEnter`, used for scenes, to read out text of the location
- `_onExit`, used for scenes, to direct the player to the next destination
- `_onAcquire`, used for objects that are meant to be kept
- `_onInpect`, used for any object when the player wishes to know more
A graph markdown was deemed insufficient for cyano, as my stories are not going to fit cleanly along
some API. What I really will need to be able to do is code it, as any good engine of any kind.
Goals for the language:
## 1. Reading as First Class
A story is only worth what its words convey, and you only have a good story if you can read it.
Imagine a file whose contents read:
```
func goToWindow:
print("You approach the window and its clear you could survive the jump");
input = get_input()
if input == "jump":
print("You fall gracefully to the ground")
else if
...
```
It honestly would be just as well to scrap this project and write a one-off in python.
The purpose of writing an entire engine would be to pull away all of the unnecessary bits and bytes
of sequencing, IO, object relations and requirements and let the writer focus on the actual writing.
Instead picture a file like this:
```
_onEnter:
You approach the window and its clear you could survive the jump
_onJump:
You fall gracefully to the ground
```
This is still the outset, so more syntax may be required, but the goal would be to make everything
else get out of the way, and reading your story takes the front seat.
## 2a. Arbitrary Functionality
A story can take us anywhere. So why would the engine that powers it only allow objects to have one of up to
a maximum of three conditions (Opened, Closed, Locked).
In reality, a chest could be Opened, Broken, Dusty, Sentient and who knows what else. The goal would be to allow
the writer to have utmost freedom in writing the story they want to tell.
## 2b. Safe Operation
However, we dont want a dynamic story to have dead ends, missing links, or endless loops. When a chest depends on
a key that doesnt exist? Thats no good. We want as many conditions as possible to be checked as soon as possible
in the writing process to help make the greatest story that has yet to be experienced.
# Cyo Spec
## Summary
Cyano's underlying framework is using callbacks. You can think of it as defining a series of triggers and effects.
Essentially, nothing happens without the user doing something, so we dont need the cyano engine to "run" code.
Instead, it should read the user input, figure out what they mean, then lookup the associated trigger that the user is tripping.
Triggers could be anything:
- Entering a room
- Combining things in their inventory
- Putting a monkey idol on a pillar
- Reaching zero health
So, with these two concepts: entities and callbacks (triggers), we can create any story with any sort of form.
Want to make it RPG? Create a player entity with stats, and create triggers like `_onKillMob`, `_onLevelUp`.
Traditional text escape adventure? Chest entity with `_onUnlock`, `_onOpen`.
## Nitty Gritty
### Entities
Defining Entities:
- A file can define multiple entities only if these definitions are qualified.
- A file cannot have unqualified and qualified definitions.
- A file can only define one set of unqualified definitions and no others.
- Entity identifiers are case-insensitive, and can only contain letters, numbers, and underscores.
Qualified Definition:
```cyo
scenes.cyo
_onEnter_LivingRoom:
_onLeave_LivingRoom:
_onExplode_LivingRoom:
```
Unqualified Definition:
```cyo
living_room.cyo
_onEnter:
_onLeave:
_onExplode:
```
Both would create internally a LivingRoom entity.
In the case of an unqualified definition, the basename of the file is used as the entities identifier. Underscores in
the filename will be removed. If an underscore is desired in the identifier, the filename would need two:
`living__room.cyo`
### Global State
Global state is accessible within any callback with said object properties TBD but would look like:
```cyo
_onRead:
if $player.wearingPendant:
The book's text comes to life!
else:
The book's dull pages seem ready to fall apart with age.
```
Attributes that would be handy for the global state to hold:
- Inventory
- Current location
- Recent action(s)
- Arbitrary stats (author defined)
Should these all reside in the `$player` object? This will probably evolve as need arises.
### Entity State
You want to open a door, but only if the pressure plate in the room is weighed down with something. Entities need to be
able to define and track arbitrary internal state. This might look something like:
```cyo
PressurePlate:
pressed=false
_onInspect_PressurePlate:
if $pressed:
"Its depressed, now what?"
else if $player.:
"Its barely noticable on the dusty stone floor, but you can just make out the lip of the plate. Something heavy could depress it."
```
### Callback Rules

View File

@ -1,7 +1,29 @@
const std = @import("std"); const std = @import("std");
const CyoContent = @import("../cyo/cyo.zig").content; const CyoContent = @import("../cyo/cyo.zig").content;
const DEFAULT_CYO_SOURCE_PATH = "cyo"; const DEFAULT_CYO_SOURCE_PATH = "./cyo";
const Token = struct { line: u32, pos: u32, lexeme: Lexeme, contents: []const u8 };
const Lexeme = enum {
Lt, // <
Gt, // >
LeftParen, // (
RightParen, // )
LeftBracket, // [
RightBracket, // ]
Text, // Foo bar blah. Another.
Dollar, // $
Period, // .
Colon, // :
Equals, // =
Hashtag, // #
Underscore, // _
Newline, // \n
Tab, // \t
Space, // ' '
Eof, //
};
pub const CyoError = error{ BadSource, BadIter }; pub const CyoError = error{ BadSource, BadIter };
@ -38,21 +60,22 @@ pub const CyoParser = struct {
var files_contents = std.StringHashMap([]const u8).init(allocator); var files_contents = std.StringHashMap([]const u8).init(allocator);
try walkDirs(allocator, cyo_dir, 0, &files_contents); try walkDirs(allocator, cyo_dir, 0, &files_contents);
var iter = files_contents.keyIterator(); var files_tokens = try lexCyoFiles(allocator, files_contents);
printFilesTokens(files_tokens);
var iter = files_tokens.keyIterator();
while (iter.next()) |key| { while (iter.next()) |key| {
const content = files_contents.get(key.*); const tokens = files_tokens.get(key.*);
if (content) |c| { if (tokens) |t| {
std.debug.print("Got key: {s}\nContent:{s}\n\n", .{ key.*, c }); t.deinit();
} else {
std.debug.print("Got key: {s}\nContent empty\n\n", .{key.*});
} }
allocator.free(key.*);
} }
files_tokens.deinit();
// var path_buf: [128]u8 = undefined;
// const cyo_dir_path = cyo_dir.realpath(pathname: []const u8, out_buffer: []u8) // const cyo_dir_path = cyo_dir.realpath(pathname: []const u8, out_buffer: []u8)
// 2. process files // 2. process files
// 2a. lexical - validate file syntax // 2a. lexical - validate file syntax
// 2b. syntactic parsing // 2b. syntactic parsing
// 2c. semantic - create objects and scenes // 2c. semantic - create objects and scenes
@ -61,6 +84,7 @@ pub const CyoParser = struct {
return CyoContent{ .allocator = allocator, .files_contents = files_contents }; return CyoContent{ .allocator = allocator, .files_contents = files_contents };
} }
// Recursively walks through directory to get all .cyo files
fn walkDirs(allocator: std.mem.Allocator, cyo_dir: std.fs.Dir, depth: u8, files_contents: *std.StringHashMap([]const u8)) !void { fn walkDirs(allocator: std.mem.Allocator, cyo_dir: std.fs.Dir, depth: u8, files_contents: *std.StringHashMap([]const u8)) !void {
var cyo_iter = cyo_dir.iterate(); var cyo_iter = cyo_dir.iterate();
while (cyo_iter.next() catch |err| { while (cyo_iter.next() catch |err| {
@ -69,11 +93,15 @@ pub const CyoParser = struct {
}) |cyo_entry| { }) |cyo_entry| {
switch (cyo_entry.kind) { switch (cyo_entry.kind) {
.file => { .file => {
// std.fs.cyo_entry.name; // only grab `.cyo` files
if (cyo_entry.name.len <= 3 or !std.mem.eql(u8, ".cyo", cyo_entry.name[cyo_entry.name.len - 4 .. cyo_entry.name.len])) {
continue;
}
for (0..depth) |_| { for (0..depth) |_| {
std.debug.print("\t", .{}); std.debug.print("\t", .{});
} }
std.debug.print("- File: {s}\n", .{cyo_entry.name}); // std.debug.print("- File: {s}\n", .{cyo_entry.name});
const file_path = try cyo_dir.realpathAlloc(allocator, cyo_entry.name); const file_path = try cyo_dir.realpathAlloc(allocator, cyo_entry.name);
var cyo_file = try std.fs.openFileAbsolute(file_path, .{ .mode = .read_only }); var cyo_file = try std.fs.openFileAbsolute(file_path, .{ .mode = .read_only });
@ -84,7 +112,7 @@ pub const CyoParser = struct {
for (0..depth) |_| { for (0..depth) |_| {
std.debug.print("\t", .{}); std.debug.print("\t", .{});
} }
std.debug.print("Dir: {s}\n", .{cyo_entry.name}); // std.debug.print("Dir: {s}\n", .{cyo_entry.name});
const dir_path = try cyo_dir.realpathAlloc(allocator, cyo_entry.name); const dir_path = try cyo_dir.realpathAlloc(allocator, cyo_entry.name);
defer allocator.free(dir_path); defer allocator.free(dir_path);
@ -92,18 +120,220 @@ pub const CyoParser = struct {
try walkDirs(allocator, new_cyo_dir, depth + 1, files_contents); try walkDirs(allocator, new_cyo_dir, depth + 1, files_contents);
}, },
else => { else => {
std.debug.print("ignoring other types...", .{}); // std.debug.print("ignoring other types...", .{});
}, },
} }
} }
} }
fn printFilesContents(files_contents: std.StringHashMap([]const u8)) void {
var iter = files_contents.keyIterator();
while (iter.next()) |key| {
const content = files_contents.get(key.*);
if (content) |c| {
std.debug.print("Got key: {s}\nContent:{s}\n\n", .{ key.*, c });
} else {
std.debug.print("Got key: {s}\nContent empty\n\n", .{key.*});
}
}
}
fn lexCyoFiles(allocator: std.mem.Allocator, files_contents: std.StringHashMap([]const u8)) !std.StringHashMap(std.ArrayList(Token)) {
var files_tokens = std.StringHashMap(std.ArrayList(Token)).init(allocator);
errdefer {
var iter = files_tokens.keyIterator();
while (iter.next()) |key| {
const tokens = files_tokens.get(key.*);
if (tokens) |t| {
t.deinit();
}
allocator.free(key.*);
}
files_tokens.deinit();
}
var iter = files_contents.keyIterator();
while (iter.next()) |key| {
const content = files_contents.get(key.*);
if (content == null) {
continue;
}
var tokens = std.ArrayList(Token).init(allocator);
const c = content.?;
var i: u32 = 0;
var line: u32 = 1;
var col: u32 = 1;
while (i < c.len) {
const lexeme = charToLexeme(c[i]);
switch (lexeme) {
// whitespace
.Space, .Tab => {
const char_repeats = greedyCapture(c, i);
for (0..char_repeats) |_| {
try tokens.append(.{ .line = line, .pos = col, .lexeme = lexeme, .contents = c[i .. i + char_repeats] });
col += 1;
}
i += char_repeats;
},
.Newline => {
try tokens.append(.{ .line = line, .pos = col, .lexeme = lexeme, .contents = c[i .. i + 1] });
line += 1;
col = 1;
i += 1;
},
// symbols
.Gt,
.Lt,
.LeftParen,
.RightParen,
.LeftBracket,
.RightBracket,
.Dollar,
.Period,
.Equals,
.Colon,
.Underscore,
=> {
try tokens.append(.{ .line = line, .pos = col, .lexeme = lexeme, .contents = c[i .. i + 1] });
// check length of content is one
std.debug.assert(tokens.items[tokens.items.len - 1].contents.len == 1);
col += 1;
i += 1;
},
// text
.Text => {
const text_length = captureText(c, i);
try tokens.append(.{ .line = line, .pos = col, .lexeme = lexeme, .contents = c[i .. i + text_length] });
col += text_length;
i += text_length;
},
.Hashtag => {
const to_end_of_line = captureLine(c, i);
// TODO for testing, remove as we dont need to save comments
try tokens.append(.{ .line = line, .pos = col, .lexeme = lexeme, .contents = c[i .. i + to_end_of_line] });
col += to_end_of_line;
i += to_end_of_line;
},
.Eof => unreachable,
}
}
// Add eof token
try tokens.append(.{ .line = line, .pos = col, .lexeme = Lexeme.Eof, .contents = "" });
// Add tokens to hashmap
const key_copy = try allocator.alloc(u8, key.len);
std.mem.copyForwards(u8, key_copy, key.*);
try files_tokens.put(key_copy, tokens);
}
return files_tokens;
}
fn printFilesTokens(files_tokens: std.StringHashMap(std.ArrayList(Token))) void {
var iter = files_tokens.keyIterator();
while (iter.next()) |key| {
std.debug.print("File: {s}", .{key.*});
const tokens = files_tokens.get(key.*);
if (tokens) |ts| {
for (ts.items) |token| {
std.debug.print("\tGot Token: {s}\tCol{d}:L{d}\t{s}\n", .{ @tagName(token.lexeme), token.pos, token.line, token.contents });
}
}
}
}
fn charToLexeme(char: u8) Lexeme {
switch (char) {
' ' => {
return .Space;
},
'\t' => {
return .Tab;
},
'\n' => {
return .Newline;
},
'<' => {
return .Lt;
},
'>' => {
return .Gt;
},
'(' => {
return .LeftParen;
},
')' => {
return .RightParen;
},
'[' => {
return .LeftBracket;
},
']' => {
return .RightBracket;
},
'$' => {
return .Dollar;
},
'.' => {
return .Period;
},
'=' => {
return .Equals;
},
':' => {
return .Colon;
},
'_' => {
return .Underscore;
},
'#' => {
return .Hashtag;
},
else => return .Text,
}
}
// given a char, it returns the number of bytes that char repeats forward starting at `i`
fn greedyCapture(seq: []const u8, i: u32) u32 {
const cap_char = seq[i];
var j = i;
while (j < seq.len and seq[j] == cap_char) {
j += 1;
}
return j - i;
}
fn captureText(seq: []const u8, i: u32) u32 {
var j = i;
while (j < seq.len and charToLexeme(seq[j]) == .Text) {
j += 1;
}
return j - i;
}
fn captureLine(seq: []const u8, i: u32) u32 {
var j = i;
while (j < seq.len) {
const lex = charToLexeme(seq[j]);
if (lex == .Newline or lex == .Eof) {
break;
}
j += 1;
}
return j - i;
}
}; };
test "parse test" { test "parse test" {
// TODO, programatically create a test directory instead of relying on the test directory to have the right contents
const cyo_test_dir_path = try std.fs.cwd().realpathAlloc(std.testing.allocator, "./test/cyo_test_dir"); const cyo_test_dir_path = try std.fs.cwd().realpathAlloc(std.testing.allocator, "./test/cyo_test_dir");
defer std.testing.allocator.free(cyo_test_dir_path); defer std.testing.allocator.free(cyo_test_dir_path);
var cyo_parser = try CyoParser.init(std.testing.allocator, cyo_test_dir_path); var cyo_parser = try CyoParser.init(std.testing.allocator, cyo_test_dir_path);
defer cyo_parser.deinit(); defer cyo_parser.deinit();
try std.testing.expectEqual(6, cyo_parser.cyo_content.files_contents.count()); // Verify reading in of correct files
try std.testing.expectEqual(7, cyo_parser.cyo_content.files_contents.count());
} }

View File

@ -0,0 +1 @@

View File

@ -1,4 +1,32 @@
[Duct Tape] DuctTape:
d:The tape that does it all. str="Duct Tape"
status=[.Full, .Empty]
location=Warehouse
[Flash Light] FlashLight:
str="Flash Light"
status=[.Bright, .Dead, .Broken]
_onExamine:
if $self.status = .Bright:
A handy tool for seeing your way out.
else if $self.status = .Dead:
A handy tool for seeing your way out, unfortunately its out of batteries.
else:
A handy tool for seeing your way out, unfortunately its busted.
_onUse:
if $context.args.contains(Batteries) and $self.status = .Dead:
$self.status = .Bright
$player.inventory.remove(Batteries)
You screw the back on tight, and the light comes on!
_onFix:
if $context.args.contains(DuctTape) and $self.status = .Broken:
$self.status = .Dead
$DuctTape.status = .Empty
Nothing duct tape cant fix! The flashlight just needs some power.
Batteries:
_onExamine:
Energized little cylinders, good for something...

View File

@ -1,17 +1,13 @@
[Warehouse] Warehouse:
_onEnter:
Your vision blurs as you fumble for the tablet. The glow of your fingers is nearly gone. The salty tablet is the last Your vision blurs as you fumble for the tablet. The glow of your fingers is nearly gone. The salty tablet is the last
sensation before everything goes dark. sensation before everything goes dark.
<pause3>
--- <clear>
You awaken. How long has it been? Thanks to your dull glow, you can read the clock on the wall. 8:36. Only a few hours. You awaken. How long has it been? Thanks to your dull glow, you can read the clock on the wall. 8:36. Only a few hours.
Thank God you brought one with you. <p1> But its not a cure, just a band-aid. <p4> Time is ticking. <p1> You've got 24 hours Thank God you brought one with you. <p1> But its not a cure, just a band-aid. <p4> Time is ticking. <p1> You've got 24 hours
to breathe, better get back before then. to breathe, better get back before then.
[Warehouse.d] _onExamine:
It might have been an Amazon warehouse. Everything seems sacked. Empty shelves twist in some corporate labrynth before you. It might have been an Amazon warehouse. Everything seems sacked. Empty shelves twist in some corporate labrynth before you.
Would have been nice to have something to show for this trip. But information isn't useless. Would have been nice to have something to show for this trip. But information isn't useless.
[Warehouse.i]
Duct Tape

View File

@ -1 +1,25 @@
[Bathroom] Bathroom:
washed=false
_onEnter:
Its a normal bathroom.
if not $washed:
Should probably wash up.
_onLook:
Bathroom complete with sink, faucet, the whole lot.
_onWash:
$washed=true
Nice cold water.
_beforeOnWash:
if $washed:
You dont need to wash again.
_block
_afterOnWash:
if not $previous.bathroom.washed:
You feel refreshed.
<pause2>
Time to get to work.

View File

@ -0,0 +1 @@
[Bathroom]