Skip to content

Commit

Permalink
Add starting bough path
Browse files Browse the repository at this point in the history
  • Loading branch information
bgk- committed Feb 29, 2024
1 parent 48d4382 commit dc8e81a
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 114 deletions.
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = "topiary",
.version = "0.8.1",
.version = "0.9.0",
.paths = .{""},
.dependencies = .{
},
Expand Down
32 changes: 16 additions & 16 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,38 @@ Tags can are also optional and can be omitted or multiple can be separated each

### Jumps

Boughs can be started with jumps, denoted with `=> [Name]`.

So with that we can write a very simple program.
Boughs can be started with jumps, denoted with `=> [Name]`. The first jump must be passed in to the `start` function,
or if using the CLI the second parameter of the run command.

```topi
# topi run file.topi START
=== START {
:John: "Hello Jane!" #greet
:Jane: "Great to see you, John"
}
```

#### Automatic Jump Starts

As a convenience, if you don't pass in a jump to the CLI the first bough in the file will be used.

=> START
```topi
# topi run file.topi // START will run in the above example
```

### Nesting

Boughs can be nested and jumped to with `.` like so:

```topi
# topi run file.topi START.INNER
=== START {
=== INNER {
:Speaker: "Start here"
}
}
=> START.INNER
```

### Forks
Expand All @@ -63,8 +71,6 @@ Fork bodies can be either a jump or a block surrounded by braces.
=== END {
:John: "This is hard"
}
=> START
```

Forks can also be named to revisit, useful if wanting to loop choices.
Expand All @@ -82,7 +88,6 @@ Forks can also be named to revisit, useful if wanting to loop choices.
=== END {
:John: "The hard way it is"
}
=> START
```

Choices can also be unique with `~*`, this means once they are visited,
Expand All @@ -103,7 +108,6 @@ they won't be added to the choice list again.
=== END {
:John: "The hard way it is"
}
=> START
```

Choices can also be named to get the visit count in the story,
Expand All @@ -126,7 +130,6 @@ to be used in your runner code.
print(START.DIFFICULTY.EASY) // everytime EASY was chosen
print(START.DIFFICULTY.HARD) // everytime HARD was chosen
}
=> START
```

### Visits
Expand Down Expand Up @@ -180,7 +183,6 @@ Visit paths can be found within scopes and don't need the full path written out.
}
}
}
=> START
```

### Flow and Jump Back Ups
Expand Down Expand Up @@ -225,7 +227,6 @@ However named forks with backups should be used with caution.
}
:Jane: "Good choice."
}
=> START
```


Expand Down Expand Up @@ -406,7 +407,6 @@ const value = 42
greeting = "Hello"
:Jane: "{greeting}, John. The password is {value}." // Hello, John. The password is 42.
}
=> START
```

### Functions
Expand Down Expand Up @@ -529,6 +529,8 @@ When making nested jumps be aware that preceeding code will be execuded to ensur
Consider the following

```topi
# topi run file.topi
// Expected output :Speaker: "End test 2"
=== START {
:Speaker: "Start conversation"
var test = "test"
Expand All @@ -538,11 +540,9 @@ Consider the following
:Speaker: "End {test}"
}
}
=> START.INNER // Expected output :Speaker: "End test 2"
```

In this situation we need to creating the `test` variable before it's set to `test 2`.
In this situation we need to create the `test` variable before it's set to `test 2`.
To ensure that happens when you jump to `START.INNER` first Topiary will jump to `START`
execute all code (while skipping dialogues and forks), then when it encounters another jump
or ends, it'll then jump to `INNER`.
Expand Down
2 changes: 0 additions & 2 deletions examples/hello.topi
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,3 @@
:: "They walk away..."
}

=> START

3 changes: 1 addition & 2 deletions examples/story.topi
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ var party = Set{"Rogue", "Druid", "Fighter", "Archer"}
var gold = 0
var spiderDefeated = false

class Character {
class Character = {
injured = false,
}
var rogue = new Character{}
var druid = new Character{}
var fighter = new Character{}
var archer = new Character{}

=> TAVERN
=== TAVERN {
:Innkeeper: "Welcome to the Rusty Tankard, travelers. A dire quest awaits those who are brave."
:Innkeeper: "A terrible curse plagues our town, and we need your help. Are you four up for the challenge?"
Expand Down
26 changes: 26 additions & 0 deletions src/bytecode.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pub const Bytecode = struct {
uuids: []UUID.ID,
locals_count: usize,
token_lines: []u32,
boughs: []BoughJump,

pub const BoughJump = struct { name: []const u8, ip: OpCode.Size(.jump) };

pub const GlobalSymbol = struct {
name: []const u8,
Expand All @@ -29,6 +32,8 @@ pub const Bytecode = struct {
allocator.free(self.uuids);
for (self.global_symbols) |s| allocator.free(s.name);
allocator.free(self.global_symbols);
for (self.boughs) |b| allocator.free(b.name);
allocator.free(self.boughs);
}

pub fn serialize(self: *Bytecode, writer: anytype) !void {
Expand All @@ -41,6 +46,13 @@ pub const Bytecode = struct {
try writer.writeByte(if (sym.is_extern) 1 else 0);
}

try writer.writeInt(u64, @as(u64, @intCast(self.boughs.len)), .little);
for (self.boughs) |bough| {
try writer.writeInt(u16, @as(u16, @intCast(bough.name.len)), .little);
try writer.writeAll(bough.name);
try writer.writeInt(OpCode.Size(.jump), bough.ip, .little);
}

try writer.writeInt(u64, @as(u64, @intCast(self.instructions.len)), .little);
try writer.writeAll(self.instructions);
try writer.writeInt(u64, @as(u64, @intCast(self.token_lines.len)), .little);
Expand Down Expand Up @@ -68,6 +80,19 @@ pub const Bytecode = struct {
};
}

const bough_count = try reader.readInt(u64, .little);
var boughs = try allocator.alloc(BoughJump, bough_count);
count = 0;
while (count < bough_count) : (count += 1) {
const length = try reader.readInt(u16, .little);
const buf = try allocator.alloc(u8, length);
try reader.readNoEof(buf);
boughs[count] = BoughJump{
.name = buf,
.ip = try reader.readInt(OpCode.Size(.jump), .little),
};
}

const instruction_count = try reader.readInt(u64, .little);
const instructions = try allocator.alloc(u8, instruction_count);
try reader.readNoEof(instructions);
Expand All @@ -91,6 +116,7 @@ pub const Bytecode = struct {
return .{
.instructions = instructions,
.token_lines = token_lines,
.boughs = boughs,
.constants = constants,
.global_symbols = global_symbols,
.uuids = uuids,
Expand Down
110 changes: 68 additions & 42 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,21 @@ fn usage(comptime msg: []const u8) !void {
if (!std.mem.eql(u8, msg, "")) {
try out.print(msg, .{});
try out.print("\n", .{});
} else {
try out.print("topi - command line topiary processor\n", .{});
try out.print("Usage:\n", .{});
try out.print("\n", .{});
}
try out.print(" topi <file> [--auto|-a <count>] [--compile|-c <output_file>]\n", .{});
try out.print("topi - command line topiary processor\n", .{});
try out.print("Usage:\n", .{});
try out.print(" topi [-v | --version] [-h | --help] <command> <file> [flags]\n", .{});
try out.print("\n", .{});
try out.print("Commands:\n", .{});
try out.print(" topi run <file> [start_bough] [--verbose]\n", .{});
try out.print(" topi auto <file> <count> [-verbose]\n", .{});
try out.print(" topi compile <file> [output_file] [--verbose] [--dry|-d]\n", .{});
try out.print("\n", .{});
try out.print("Flags:\n", .{});
try out.print("\n", .{});
try out.print(" --version, -v: Output current version\n", .{});
try out.print(" --verbose: Output debug logs\n", .{});
try out.print(" --dry, -d: Compile only\n", .{});
}

pub fn main() !void {
Expand All @@ -32,51 +41,64 @@ pub fn main() !void {
var args = try std.process.argsWithAllocator(arena.allocator());
_ = args.skip();

const file_path = args.next();
if (file_path == null) {
try usage("No file argument provided.\n");
return;
}
if (std.mem.eql(u8, file_path.?, "-h") or std.mem.eql(u8, file_path.?, "--help")) {
try usage("");
return;
const maybe_cmd = args.next();
if (maybe_cmd == null) return usage("");
const cmd = maybe_cmd.?;

const is_version = std.mem.eql(u8, cmd, "-v") or std.mem.eql(u8, cmd, "--version");
if (is_version) return usage("");
const is_help = std.mem.eql(u8, cmd, "-h") or std.mem.eql(u8, cmd, "--help");
if (is_help) return usage("");

const is_run = std.mem.eql(u8, cmd, "run");
const is_auto = std.mem.eql(u8, cmd, "auto");
const is_compile = std.mem.eql(u8, cmd, "compile");
if (!is_run and !is_auto and !is_compile) return usage("Unknown command");

const maybe_file_path = args.next();
if (maybe_file_path == null) {
return usage("No file argument provided.\n");
}
var is_compile = false;
const file_path = maybe_file_path.?;

var out_path: ?[]const u8 = null;
var is_auto = false;
var bough_path: ?[]const u8 = null;
var auto_count: usize = 0;
var is_dry = false;
var is_verbose = false;
while (args.next()) |flag| {
if (std.mem.eql(u8, flag, "-c") or std.mem.eql(u8, flag, "--compile")) {
const maybe_out_file = args.next();
if (maybe_out_file == null) {
try usage("Compile requires an output file.\n");
return;
}
is_compile = true;
out_path = maybe_out_file.?;
}
if (std.mem.eql(u8, flag, "-a") or std.mem.eql(u8, flag, "--auto")) {
const maybe_auto_count = args.next();
if (maybe_auto_count == null) {
try usage("Auto requires a play count.\n");
return;

if (is_run) {
bough_path = args.next();
if (bough_path) |b| {
if (std.mem.eql(u8, b, "--verbose")) {
bough_path = null;
is_verbose = true;
}
is_auto = true;
auto_count = try std.fmt.parseInt(u64, maybe_auto_count.?, 10);
}
if (std.mem.eql(u8, flag, "-d") or std.mem.eql(u8, flag, "--dry")) {
is_dry = true;
}
if (std.mem.eql(u8, flag, "-v") or std.mem.eql(u8, flag, "--verbose")) {
}
if (is_compile) {
out_path = args.next();
if (out_path == null) return usage("Compile requires an output path or --dry flag.");
is_dry = std.mem.eql(u8, out_path.?, "--dry") or std.mem.eql(u8, out_path.?, "-d");
}

if (is_auto) {
const maybe_count = args.next();
if (maybe_count == null) return usage("Auto requires a play count.\n");
auto_count = std.fmt.parseInt(u64, maybe_count.?, 10) catch {
return usage("Invalid auto count specified");
};
}
const flag = args.next();
if (flag) |f| {
if (std.mem.eql(u8, f, "--verbose")) {
is_verbose = true;
}
}

const allocator = arena.allocator();
const full_path = std.fs.cwd().realpathAlloc(allocator, file_path.?) catch |err| {
try std.io.getStdErr().writer().print("Could not find file at {s}", .{file_path.?});
const full_path = std.fs.cwd().realpathAlloc(allocator, file_path) catch |err| {
try std.io.getStdErr().writer().print("Could not find file at {s}", .{file_path});
if (is_verbose) return err;
return;
};
Expand Down Expand Up @@ -117,7 +139,7 @@ pub fn main() !void {
var auto_runner = AutoTestRunner.init();
var vm = try Vm.init(vm_alloc, bytecode, &auto_runner.runner);
vm.interpret() catch {
vm.err.print(std.debug);
vm.err.print(std.io.getStdErr().writer());
continue;
};
defer vm.deinit();
Expand All @@ -144,9 +166,13 @@ pub fn main() !void {
return;
};

vm.interpret() catch {
vm.err.print(std.debug);
};
try vm.start(bough_path);
while (vm.can_continue) {
vm.run() catch {
vm.err.print(std.io.getStdErr().writer());
break;
};
}
}

fn getFilePath(allocator: std.mem.Allocator) !?[]const u8 {
Expand Down
16 changes: 16 additions & 0 deletions src/compiler.test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,22 @@ test "Classes" {
}
}

test "Global Jump Error" {
const input =
\\ === START {}
\\ => START
;

var mod = Module.create(allocator);
defer mod.deinit();
defer mod.entry.source_loaded = false;
const err = compileSource(input, &mod);
const errWriter = std.io.getStdIn().writer();
try mod.writeErrors(errWriter);

try testing.expect(Compiler.Error.CompilerError == err);
}

test "Serialize" {
const input =
\\ var str = "string value"
Expand Down
Loading

0 comments on commit dc8e81a

Please sign in to comment.