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
779c5c0
feat: add arithmetic operations to set()
solara404 Apr 20, 2026
db4af86
feature: add 'rest' functionality to parseAndRun()
solara404 Apr 24, 2026
e213bd1
feature: add integer calc() to set()
solara404 Apr 27, 2026
c314e71
feature: calc() calculate as float and round to int
solara404 Apr 27, 2026
d9837d1
feature: add '(' and ')' to calc()
solara404 Apr 27, 2026
f90abe8
feature: add variable support to calc()
solara404 Apr 27, 2026
342bb6a
fix: self.pos == self.input.len error
solara404 Apr 27, 2026
0a3adc9
refactor: `rest` parameter doc comment
solara404 Apr 28, 2026
d60a403
refactor: add doc comments to CalcParser struct
solara404 Apr 28, 2026
f6943e6
refactor: set() single info message
solara404 Apr 28, 2026
aabb14a
refactor: remove debug message
solara404 Apr 28, 2026
d30715d
refactor: set() long description change
solara404 Apr 29, 2026
7070296
feature: calc() multiply terms without '*' between
solara404 Apr 30, 2026
86fdbbd
fix: variables have to start alphabetic
solara404 Apr 30, 2026
9144f36
refactor: update set() long description
solara404 May 6, 2026
1cd0f22
fix: set() simple assign trim end whitespaces
solara404 May 6, 2026
8eb5e36
fix: make set() reject simple assign values containing whitespace
solara404 May 6, 2026
06273b2
test: Ensure `rest` parameter positions
aaumar25 May 7, 2026
9cf6346
refactor: fix typo
solara404 May 7, 2026
fc17379
refactor: remove comments
solara404 May 7, 2026
a1b60c0
refactor: clarify set() long description
solara404 May 7, 2026
1cd86c9
refactor: change parseVariable() log level to debug
solara404 May 7, 2026
2317b27
refactor: remove unnecessary digit check from set() parameter
solara404 May 7, 2026
cbaba4c
refactor: init set() `result` variable as empty slice
solara404 May 7, 2026
4614f02
refactor: change calc() return type from i32 to f32
solara404 May 8, 2026
502fdac
fix: param rest ensure parseAndRun() iterator is empty
solara404 May 8, 2026
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
301 changes: 277 additions & 24 deletions src/command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ pub const Command = union(enum) {
optional: bool = false,
quotable: bool = true,
resolve: bool = true,
/// If true, this parameter consumes all remaining input as a single
/// value. This parameter can only be used as the last parameter.
rest: bool = false,

fn resolveKind() type {
// Calculate how many parameters are there that has parsing rule.
Expand Down Expand Up @@ -404,20 +407,40 @@ pub fn init() !void {
.long_description = "Clear visible screen output.",
.execute = &clear,
} });
try registry.put(.{ .executable = .{
.name = "SET",
.parameters = &[_]Command.Executable.Parameter{
.{ .name = "variable", .resolve = false },
.{ .name = "value" },
try registry.put(.{
.executable = .{
.name = "SET",
.parameters = &[_]Command.Executable.Parameter{
.{ .name = "name", .resolve = false },
.{ .name = "value", .resolve = false, .rest = true },
},
.short_description = "Set a variable equal to a value.",
.long_description =
\\Create or update a variable name that resolves to the provided value
\\in all future commands. If a variable name and a value is provided,
\\then the variable is set directly to the value. If an `=` symbol
\\is provided as the first element of the value parameter, the result
\\of the expression is assigned. Variable names are case sensitive
\\and have to begin with a letter.
\\
\\Example: Set variable 'var' to the value 5 and variable 'var2' to
\\value 'line1'.
\\SET var 5
\\SET var2 line1
\\
\\Example: Set variable 'var' to value of variable 'var' plus 1.
\\SET var = var + 1
\\
\\Supported operators:
\\* Addition +
\\* Subtraction -
\\* Multiplication *
\\* Division /
\\* Modulo %
,
.execute = &set,
},
.short_description = "Set a variable equal to a value.",
.long_description =
\\Create or update a variable name that resolves to the provided value
\\in all future commands. Variable names are case sensitive and shall
\\not begin with digit.
,
.execute = &set,
} });
});
try registry.put(.{ .executable = .{
.name = "GET",
.parameters = &[_]Command.Executable.Parameter{
Expand Down Expand Up @@ -564,6 +587,18 @@ pub fn init() !void {
} });
}

test init {
try init();
defer deinit();
for (registry.values()) |executable| {
for (executable.parameters, 1..) |param, i| {
if (param.rest and i != executable.parameters.len) {
return error.FoundInvalidRestParameter;
}
}
}
}

pub fn deinit() void {
deinitModules();
stop.store(true, .monotonic);
Expand Down Expand Up @@ -636,10 +671,7 @@ fn parseAndRun(input: []const u8) !void {
var command: *Command.Executable = undefined;
var command_buf: [256]u8 = undefined;
if (token_iterator.next()) |token| {
if (registry.getPtr(std.ascii.upperString(
&command_buf,
token,
))) |c| {
if (registry.getPtr(std.ascii.upperString(&command_buf, token))) |c| {
command = c;
} else return error.InvalidCommand;
} else return;
Expand Down Expand Up @@ -685,11 +717,21 @@ fn parseAndRun(input: []const u8) !void {
}
params[i] = input[start_ind .. start_ind + len];
} else params[i] = token;
} else {
params[i] = token;
} else params[i] = token;

if (param.rest) {
params[i] = token_iterator.rest();
while (token_iterator.next()) |_| {} else break;
}
}
if (token_iterator.peek() != null) return error.UnexpectedParameter;

const is_rest: bool =
command.parameters.len > 0 and
command.parameters[command.parameters.len - 1].rest;

if (!is_rest and token_iterator.peek() != null)
return error.UnexpectedParameter;
Comment thread
solara404 marked this conversation as resolved.

try command.execute(params);
}

Expand Down Expand Up @@ -778,13 +820,224 @@ fn version(_: [][]const u8) !void {
}

fn set(params: [][]const u8) !void {
if (std.ascii.isDigit(params[0][0])) return error.InvalidParameter;
try variables.put(params[0], params[1]);
if (!std.ascii.isAlphabetic(params[0][0])) return error.InvalidParameter;
const name: []const u8 = params[0];
const value: []const u8 = params[1];
var result: []const u8 = &.{};

if (value[0] == '=') {
// Compute and assign
var buf: [
std.fmt.float.bufferSize(.decimal, @TypeOf(try calc("1")))
]u8 = undefined;
const res = try calc(value[1..]);
result = if (res == @round(res))
try std.fmt.bufPrint(&buf, "{d:.0}", .{res})
else
try std.fmt.bufPrint(&buf, "{d:.2}", .{res});
} else {
// Simple assign
result = std.mem.trimEnd(u8, value, &std.ascii.whitespace);
if (std.mem.indexOfScalar(u8, result, ' ') != null)
return error.InvalidParameter;
}
std.log.info("Variable '{s}': {s}\n", .{ name, result });
try variables.put(name, result);
}

const CalcError = error{
DivisionByZero,
ExpectedClosingParentheses,
TrailingCharacters,
InvalidCharacter,
ExpectedNumber,
UndefinedVariable,
InvalidVariableValue,
};

const CalcParser = struct {
input: []const u8,
pos: usize = 0,

/// Returns a slice of the current character, or null when end of input is
/// reached. Does not advance to next character.
fn peek(self: *CalcParser) ?u8 {
if (self.pos >= self.input.len) return null;
return self.input[self.pos];
}

fn skipSpaces(self: *CalcParser) void {
while (self.pos < self.input.len and
std.ascii.isWhitespace(self.input[self.pos])) self.pos += 1;
}

/// Consume `char` if appears.
fn consume(self: *CalcParser, char: u8) bool {
self.skipSpaces();
if (self.pos < self.input.len and self.input[self.pos] == char) {
self.pos += 1;
return true;
}
return false;
}

/// Parse addition and substraction
fn parseExpression(self: *CalcParser) CalcError!f32 {
var lhs = try self.parseTerm();

while (true) {
self.skipSpaces();
const op = self.peek() orelse break;
if (op != '+' and op != '-') break;

self.pos += 1;
const rhs = try self.parseTerm();
lhs = switch (op) {
'+' => lhs + rhs,
'-' => lhs - rhs,
else => unreachable,
};
}
return lhs;
}

/// Parse multiplication and division
fn parseTerm(self: *CalcParser) CalcError!f32 {
var lhs = try self.parseFactor();

while (true) {
self.skipSpaces();
const op = self.peek() orelse break;

if (std.ascii.isAlphabetic(op) or op == '(') {
lhs *= try self.parseFactor();
continue;
}

if (op != '*' and op != '/' and op != '%') break;

self.pos += 1;
const rhs = try self.parseFactor();
lhs = switch (op) {
'*' => lhs * rhs,
'/' => blk: {
if (rhs == 0.0) return error.DivisionByZero;
break :blk lhs / rhs;
},
'%' => blk: {
if (rhs == 0.0) return error.DivisionByZero;
break :blk @mod(lhs, rhs);
},
else => unreachable,
};
}
return lhs;
}

/// Parse unary operation, parentheses, variables and numbers
fn parseFactor(self: *CalcParser) CalcError!f32 {
self.skipSpaces();

if (self.consume('+')) return try self.parseFactor();
if (self.consume('-')) return -try self.parseFactor();
Comment thread
solara404 marked this conversation as resolved.

if (self.consume('(')) {
const value = try self.parseExpression();
if (!self.consume(')')) return error.ExpectedClosingParentheses;
return value;
}

const c = self.peek() orelse return error.ExpectedNumber;
if (std.ascii.isAlphabetic(c)) return self.parseVariable();
return self.parseNumber();
Comment thread
solara404 marked this conversation as resolved.
}

fn parseNumber(self: *CalcParser) CalcError!f32 {
self.skipSpaces();
const start = self.pos;

while (self.pos < self.input.len and
(std.ascii.isDigit(self.input[self.pos]) or self.input[self.pos] == '.'))
self.pos += 1;

if (self.pos == start) return error.ExpectedNumber;

return std.fmt.parseFloat(f32, self.input[start..self.pos]) catch
return error.ExpectedNumber;
}

fn parseVariable(self: *CalcParser) CalcError!f32 {
self.skipSpaces();
const start = self.pos;

if (self.pos >= self.input.len) return error.ExpectedNumber;
if (!std.ascii.isAlphabetic(self.input[self.pos]))
return error.InvalidCharacter;

while (self.pos < self.input.len and
(std.ascii.isAlphanumeric(self.input[self.pos]) or
'_' == self.input[self.pos])) self.pos += 1;

const name = self.input[start..self.pos];
const value_string = variables.get(name) orelse
return error.UndefinedVariable;
std.log.debug("{s}: {s}", .{ name, value_string });

return std.fmt.parseFloat(f32, value_string) catch {
return error.InvalidVariableValue;
};
}
};

pub fn calc(input: []const u8) CalcError!f32 {
var parser = CalcParser{ .input = input };

const value = try parser.parseExpression();

parser.skipSpaces();
if (parser.pos != parser.input.len) return error.TrailingCharacters;
return value;
}

test "calc" {
try std.testing.expectEqual(14, calc("2 + 3 * 4"));
try std.testing.expectEqual(30, calc(" 20 + 5 * 2 "));
try std.testing.expectEqual(5, calc("17 % 5 + 6 / 2"));
try std.testing.expectEqual(5, calc("17%5+6/2"));
try std.testing.expectEqual(0.375, calc("1/8 + 2/8"));
try std.testing.expectEqual(72, calc("(2+2)*2*(3+3*2)"));
try std.testing.expectEqual(12, calc("2 + 2 (3 + 2)"));
try std.testing.expectEqual(24, calc("(1+1) (3 + 1)(2+ 3/3)"));
try std.testing.expectEqual(0.1, calc(".1"));
try std.testing.expectEqual(0.1, calc("0.1"));
try std.testing.expectEqual(1.25, calc("1.25"));
try std.testing.expectEqual(100.25, calc("100.25"));
try std.testing.expectEqual(1, calc("1."));
try std.testing.expectEqual(2.5, calc(".5 + 2"));
try std.testing.expectEqual(14, calc("2 - -3 * 4"));
try std.testing.expectEqual(14, calc("2 --3 * 4"));
try std.testing.expectEqual(14, calc("2 - (-3) * 4"));

try std.testing.expectError(error.DivisionByZero, calc("2/0"));
try std.testing.expectError(error.DivisionByZero, calc("2%0"));
try std.testing.expectError(error.ExpectedClosingParentheses, calc("(((2+1)*(((1+1))))*(((2-1)))"));
try std.testing.expectError(error.ExpectedClosingParentheses, calc("2 +2*( 2-1"));
try std.testing.expectError(error.ExpectedClosingParentheses, calc("(2 +2*( 2-1) + 2"));
try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 5"));
try std.testing.expectError(error.TrailingCharacters, calc("(5+1)2"));
try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 )"));
try std.testing.expectError(error.TrailingCharacters, calc("2 + 2 @"));
try std.testing.expectError(error.ExpectedNumber, calc("2+2+"));
try std.testing.expectError(error.ExpectedNumber, calc("2+ @"));
try std.testing.expectError(error.ExpectedNumber, calc("."));
try std.testing.expectError(error.ExpectedNumber, calc(". + 1"));
try std.testing.expectError(error.ExpectedNumber, calc("1.."));
try std.testing.expectError(error.ExpectedNumber, calc("1.2.3"));
}

fn get(params: [][]const u8) !void {
if (variables.get(params[0])) |value| {
std.log.info("Variable \"{s}\": {s}\n", .{
std.log.info("Variable '{s}': {s}\n", .{
params[0],
value,
});
Expand All @@ -795,7 +1048,7 @@ fn remove(params: [][]const u8) !void {
if (std.ascii.isDigit(params[0][0])) {
return error.InvalidParameter;
} else if (variables.get(params[0])) |value| {
std.log.info("Remove variable \"{s}\": {s}\n", .{
std.log.info("Remove variable '{s}': {s}\n", .{
params[0],
value,
});
Expand Down
13 changes: 13 additions & 0 deletions src/modules/mes07.zig
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ pub fn init(_: Config) !void {
} });
}

test init {
try command.init();
try init(.{});
defer command.deinit();
for (command.registry.values()) |executable| {
for (executable.parameters, 1..) |param, i| {
if (param.rest and i != executable.parameters.len) {
return error.FoundInvalidRestParameter;
}
}
}
}

pub fn deinit() void {
if (connection.len > 0) {
while (processing.load(.monotonic)) {
Expand Down
13 changes: 13 additions & 0 deletions src/modules/mmc_client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,19 @@ pub fn init(c: Config) !void {
errdefer command.registry.orderedRemove("SET_CARRIER_ID");
}

test init {
const dummy_config: Config = .{ .host = &.{}, .port = 0 };
try command.init();
try init(dummy_config);
defer command.deinit();
for (command.registry.values()) |executable| {
for (executable.parameters, 1..) |param, i| {
if (param.rest and i != executable.parameters.len) {
return error.FoundInvalidRestParameter;
}
}
}
}
pub fn deinit() void {
commands.disconnect.impl(&.{}) catch {};
parameter.deinit();
Expand Down
Loading